From 5d9b227a2ddcd157d46361f7201d909fe2534a3e Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Tue, 26 Sep 2017 14:44:36 -0400 Subject: [PATCH 01/26] Add relative time queries --- spec/MongoTransform.spec.js | 46 ++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 94 +++++++++++++++++++- 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 4c99f658f2..f116c5e61c 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -347,3 +347,49 @@ describe('transformUpdate', () => { done(); }); }); + +describe('naturalTimeToDate', () => { + 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'; + const { result, status, info } = transform.naturalTimeToDate(text, now); + expect(result.toISOString()).toBe('2017-10-08T23:52:16.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 ago'; + const { result, status, info } = transform.naturalTimeToDate(text, now); + expect(result.toISOString()).toBe('2017-09-24T01:27:16.617Z'); + expect(status).toBe('success'); + expect(info).toBe('past'); + }); + }); + + describe('Error cases', () => { + it('should error if string contains neither `ago` nor `in`', () => { + expect(transform.naturalTimeToDate('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.naturalTimeToDate('in 12 hours 1')).toEqual({ + status: 'error', + info: 'Invalid time string. Dangling unit or number.', + }); + + expect(transform.naturalTimeToDate('12 hours minute ago')).toEqual({ + status: 'error', + info: 'Invalid time string. Dangling unit or number.', + }); + }); + }); +}); + diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index d745668c80..af8d50d2f6 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -533,6 +533,86 @@ function transformTopLevelAtom(atom, field) { } } +function naturalTimeToDate(text, now = new Date()) { + let parts = text.split(' '); + if (!parts.length) { + return { status: 'invalid', info: 'Not a time string' }; + } + + // Filter out whitespace + parts = parts.filter((part) => part !== ''); + + const future = parts[0] === 'in'; + const past = parts[parts.length - 1] === 'ago'; + + if (!future && !past) { + return { status: 'error', info: "Time should either start with 'in' or end with 'ago'" }; + } + + // strip the 'ago' or 'in' + if (future) { + parts = parts.slice(1); + } else if (past) { + parts = parts.slice(0, parts.length - 1); + } + + if (parts.length % 2 !== 0) { + return { + status: 'error', + info: 'Invalid time string. Dangling unit or number.', + }; + } + + const pairs = []; + while(parts.length) { + pairs.push([ parts.shift(), parts.shift() ]); + } + + const seconds = pairs.reduce((sum, [num, interval]) => { + const val = parseInt(num, 10); + + switch(interval) { + case 'day': + case 'days': + sum += val * 24 * 60 * 60; + break; + + case 'hour': + case 'hours': + sum += val * 60 * 60; + break; + + case 'minute': + case 'minutes': + sum += val * 60; + break; + + case 'second': + case 'seconds': + sum += val; + break; + } + + return sum; + }, 0); + + const milliseconds = seconds * 1000; + if (future) { + return { + status: 'success', + info: 'future', + result: new Date(now.valueOf() + milliseconds) + }; + } + if (past) { + return { + status: 'success', + info: 'past', + result: new Date(now.valueOf() - milliseconds) + }; + } +} + // Transforms a query constraint from REST API format to Mongo format. // A constraint is something with fields like $lt. // If it is not a valid constraint but it could be a valid something @@ -565,9 +645,20 @@ function transformConstraint(constraint, field) { case '$gte': case '$exists': case '$ne': - case '$eq': + case '$eq': { + const text = constraint[key]; + if (typeof text === 'string') { + const parserResult = naturalTimeToDate(text); + + if (parserResult.status === 'success') { + answer[key] = parserResult.result; + break; + } + } + answer[key] = transformer(constraint[key]); break; + } case '$in': case '$nin': { @@ -1196,4 +1287,5 @@ module.exports = { transformUpdate, transformWhere, mongoObjectToParseObject, + naturalTimeToDate, }; From 9b88e132df1c5329eeb9edd24b174fe4764ec9fb Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Mon, 2 Oct 2017 16:53:56 -0400 Subject: [PATCH 02/26] Encode successful result --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index af8d50d2f6..2548012361 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -651,7 +651,7 @@ function transformConstraint(constraint, field) { const parserResult = naturalTimeToDate(text); if (parserResult.status === 'success') { - answer[key] = parserResult.result; + answer[key] = Parse._encode(parserResult.result); break; } } From 205fc3577c00e04553c8d22071f1d0366152c838 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 11 Oct 2017 14:24:58 -0400 Subject: [PATCH 03/26] Add integration test --- spec/.eslintrc.json | 3 +- spec/ParseQuery.spec.js | 43 +++++++++++++++++++- spec/helper.js | 23 +++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 3 +- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index 32b0a12f32..7153fae646 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -24,7 +24,8 @@ "expectError": true, "jequal": true, "create": true, - "arrayContains": true + "arrayContains": true, + "dropDatabase": true }, "rules": { "no-console": [0] diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index eb82afab60..e4ccb78a21 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3105,7 +3105,46 @@ describe('Parse.Query testing', () => { equal(result.has('testPointerField'), result.get('shouldBe')); }); done(); - } - ).catch(done.fail); + }).catch(done.fail); + }); + + it('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 + }); + + dropDatabase() + .then(() => Parse.Object.saveAll([obj1, obj2])) + .then(() => { + const q = new Parse.Query('MyCustomObject'); + q.greaterThan('ttl', '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', '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', '5 days ago'); + return q.find({ useMasterKey: true }); + }) + .then((results) => { + expect(results.length).toBe(0); + }) + .then(done, done.fail); }); }); diff --git a/spec/helper.js b/spec/helper.js index 83e0dd22a1..060de28c6e 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -451,3 +451,26 @@ jasmine.restoreLibrary = function(library, name) { } require(library)[name] = libraryCache[library][name]; } + +const { MongoClient } = require('mongodb'); +global.dropDatabase = () => { + const connectionPromise = new Promise((resolve, reject) => + MongoClient.connect(mongoURI, (err, db) => { + if (err) { + reject(err); + } else { + resolve(db); + } + })); + + return connectionPromise.then((connection) => + new Promise((resolve, reject) => { + connection.dropDatabase((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + })); +}; diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 2548012361..56d1e0b0db 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -649,9 +649,8 @@ function transformConstraint(constraint, field) { const text = constraint[key]; if (typeof text === 'string') { const parserResult = naturalTimeToDate(text); - if (parserResult.status === 'success') { - answer[key] = Parse._encode(parserResult.result); + answer[key] = new Date(parserResult.result); break; } } From 712c495ac2b4aeb8f324366d3f2e760f2edc173c Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 11 Oct 2017 14:45:46 -0400 Subject: [PATCH 04/26] Add more error cases --- spec/MongoTransform.spec.js | 14 ++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 29 ++++++++++++++------ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index f116c5e61c..0f44346677 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -390,6 +390,20 @@ describe('naturalTimeToDate', () => { info: 'Invalid time string. Dangling unit or number.', }); }); + + it('should error if numbers are invalid', () => { + expect(transform.naturalTimeToDate('12 hours 123a minute ago')).toEqual({ + status: 'error', + info: "'123a' is not an integer.", + }); + }); + + it('should error on invalid interval units', () => { + expect(transform.naturalTimeToDate('4 score 7 years ago')).toEqual({ + status: 'error', + info: "Invalid interval: 'score'", + }); + }); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 56d1e0b0db..a8bc3552d8 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -568,33 +568,44 @@ function naturalTimeToDate(text, now = new Date()) { pairs.push([ parts.shift(), parts.shift() ]); } - const seconds = pairs.reduce((sum, [num, interval]) => { - const val = parseInt(num, 10); + let seconds = 0; + for (const [num, interval] of pairs) { + const val = Number(num); + if (!Number.isInteger(val)) { + return { + status: 'error', + info: `'${num}' is not an integer.`, + }; + } switch(interval) { case 'day': case 'days': - sum += val * 24 * 60 * 60; + seconds += val * 24 * 60 * 60; break; case 'hour': case 'hours': - sum += val * 60 * 60; + seconds += val * 60 * 60; break; case 'minute': case 'minutes': - sum += val * 60; + seconds += val * 60; break; case 'second': case 'seconds': - sum += val; + seconds += val; break; - } - return sum; - }, 0); + default: + return { + status: 'error', + info: `Invalid interval: '${interval}'`, + }; + } + } const milliseconds = seconds * 1000; if (future) { From f3fce460db42b97cd1cd6bd1c4e3737359b9fee3 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 11 Oct 2017 14:52:59 -0400 Subject: [PATCH 05/26] Remove unnecessary new Date --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index a8bc3552d8..af7d5e7ba3 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -661,7 +661,7 @@ function transformConstraint(constraint, field) { if (typeof text === 'string') { const parserResult = naturalTimeToDate(text); if (parserResult.status === 'success') { - answer[key] = new Date(parserResult.result); + answer[key] = parserResult.result; break; } } From b837e2a518c8d4cd6ab355c7a61661247fe43e42 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Thu, 12 Oct 2017 14:58:24 -0400 Subject: [PATCH 06/26] Error when time has both 'in' and 'ago' --- spec/MongoTransform.spec.js | 7 +++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 0f44346677..1f66bd290f 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -404,6 +404,13 @@ describe('naturalTimeToDate', () => { info: "Invalid interval: 'score'", }); }); + + it("should error when string contains 'ago' and 'in'", () => { + expect(transform.naturalTimeToDate('in 1 day 2 minutes ago')).toEqual({ + status: 'error', + info: "Time cannot have both 'in' and 'ago'", + }); + }); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index af7d5e7ba3..48afea164d 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -549,6 +549,13 @@ function naturalTimeToDate(text, now = new Date()) { return { status: 'error', info: "Time should either start with 'in' or end with 'ago'" }; } + if (future && past) { + return { + status: 'error', + info: "Time cannot have both 'in' and 'ago'", + }; + } + // strip the 'ago' or 'in' if (future) { parts = parts.slice(1); @@ -664,6 +671,10 @@ function transformConstraint(constraint, field) { answer[key] = parserResult.result; break; } + + if (parserResult.status === 'error') { + log.info('Error while parsing relative date', parserResult); + } } answer[key] = transformer(constraint[key]); From f4723a6b419a6fc0cacd0c1ff1fa8467e651235d Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Fri, 13 Oct 2017 09:11:07 -0400 Subject: [PATCH 07/26] naturalTimeToDate -> relativeTimeToDate --- spec/MongoTransform.spec.js | 18 +++++++++--------- src/Adapters/Storage/Mongo/MongoTransform.js | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 1f66bd290f..29c71651e8 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -348,13 +348,13 @@ describe('transformUpdate', () => { }); }); -describe('naturalTimeToDate', () => { +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'; - const { result, status, info } = transform.naturalTimeToDate(text, now); + const { result, status, info } = transform.relativeTimeToDate(text, now); expect(result.toISOString()).toBe('2017-10-08T23:52:16.617Z'); expect(status).toBe('success'); expect(info).toBe('future'); @@ -364,7 +364,7 @@ describe('naturalTimeToDate', () => { describe('In the past', () => { it('should parse valid natural time', () => { const text = '2 days 12 hours 1 minute ago'; - const { result, status, info } = transform.naturalTimeToDate(text, now); + const { result, status, info } = transform.relativeTimeToDate(text, now); expect(result.toISOString()).toBe('2017-09-24T01:27:16.617Z'); expect(status).toBe('success'); expect(info).toBe('past'); @@ -373,40 +373,40 @@ describe('naturalTimeToDate', () => { describe('Error cases', () => { it('should error if string contains neither `ago` nor `in`', () => { - expect(transform.naturalTimeToDate('12 hours 1 minute')).toEqual({ + 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.naturalTimeToDate('in 12 hours 1')).toEqual({ + expect(transform.relativeTimeToDate('in 12 hours 1')).toEqual({ status: 'error', info: 'Invalid time string. Dangling unit or number.', }); - expect(transform.naturalTimeToDate('12 hours minute ago')).toEqual({ + expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({ status: 'error', info: 'Invalid time string. Dangling unit or number.', }); }); it('should error if numbers are invalid', () => { - expect(transform.naturalTimeToDate('12 hours 123a minute ago')).toEqual({ + 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.naturalTimeToDate('4 score 7 years ago')).toEqual({ + 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.naturalTimeToDate('in 1 day 2 minutes ago')).toEqual({ + expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({ status: 'error', info: "Time cannot have both 'in' and 'ago'", }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 48afea164d..8cd01f92a2 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -533,7 +533,7 @@ function transformTopLevelAtom(atom, field) { } } -function naturalTimeToDate(text, now = new Date()) { +function relativeTimeToDate(text, now = new Date()) { let parts = text.split(' '); if (!parts.length) { return { status: 'invalid', info: 'Not a time string' }; @@ -666,7 +666,7 @@ function transformConstraint(constraint, field) { case '$eq': { const text = constraint[key]; if (typeof text === 'string') { - const parserResult = naturalTimeToDate(text); + const parserResult = relativeTimeToDate(text); if (parserResult.status === 'success') { answer[key] = parserResult.result; break; @@ -1308,5 +1308,5 @@ module.exports = { transformUpdate, transformWhere, mongoObjectToParseObject, - naturalTimeToDate, + relativeTimeToDate, }; From 477bbab60dfd55a65e81f817e3a11970c8da9d01 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Tue, 24 Oct 2017 14:54:52 -0400 Subject: [PATCH 08/26] Add $relativeTime operator --- spec/ParseQuery.spec.js | 6 +++--- src/Adapters/Storage/Mongo/MongoTransform.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index e4ccb78a21..fea1fd8466 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3123,7 +3123,7 @@ describe('Parse.Query testing', () => { .then(() => Parse.Object.saveAll([obj1, obj2])) .then(() => { const q = new Parse.Query('MyCustomObject'); - q.greaterThan('ttl', 'in 1 day'); + q.greaterThan('ttl', { $relativeTime: 'in 1 day' }); return q.find({ useMasterKey: true }); }) .then((results) => { @@ -3131,7 +3131,7 @@ describe('Parse.Query testing', () => { }) .then(() => { const q = new Parse.Query('MyCustomObject'); - q.greaterThan('ttl', '1 day ago'); + q.greaterThan('ttl', { $relativeTime: '1 day ago' }); return q.find({ useMasterKey: true }); }) .then((results) => { @@ -3139,7 +3139,7 @@ describe('Parse.Query testing', () => { }) .then(() => { const q = new Parse.Query('MyCustomObject'); - q.lessThan('ttl', '5 days ago'); + q.lessThan('ttl', { $relativeTime: '5 days ago' }); return q.find({ useMasterKey: true }); }) .then((results) => { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 8cd01f92a2..49a7da9925 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -664,9 +664,9 @@ function transformConstraint(constraint, field) { case '$exists': case '$ne': case '$eq': { - const text = constraint[key]; - if (typeof text === 'string') { - const parserResult = relativeTimeToDate(text); + const val = constraint[key]; + if (typeof val === 'object' && val.$relativeTime) { + const parserResult = relativeTimeToDate(val.$relativeTime); if (parserResult.status === 'success') { answer[key] = parserResult.result; break; From f28f3fd024242c02ad38b802344e10902add6cc5 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Tue, 24 Oct 2017 15:00:50 -0400 Subject: [PATCH 09/26] Throw error if $relativeTime is invalid --- src/Adapters/Storage/Mongo/MongoTransform.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 49a7da9925..791b7db605 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -536,7 +536,7 @@ function transformTopLevelAtom(atom, field) { function relativeTimeToDate(text, now = new Date()) { let parts = text.split(' '); if (!parts.length) { - return { status: 'invalid', info: 'Not a time string' }; + return { status: 'error', info: 'Not a time string' }; } // Filter out whitespace @@ -672,9 +672,8 @@ function transformConstraint(constraint, field) { break; } - if (parserResult.status === 'error') { - log.info('Error while parsing relative date', parserResult); - } + log.info('Error while parsing relative date', parserResult); + throw new Parse.Error(Parse.Error.INVALID_JSON, `bad $relativeTime (${key}) value. ${parserResult.info}`); } answer[key] = transformer(constraint[key]); From ced46cb109383c1fe907f65843284106f0dd181e Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Tue, 24 Oct 2017 15:17:15 -0400 Subject: [PATCH 10/26] Add integration test for invalid relative time --- spec/ParseQuery.spec.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index fea1fd8466..e780e54d10 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3147,4 +3147,17 @@ describe('Parse.Query testing', () => { }) .then(done, done.fail); }); + + it('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' }); + return obj1.save({ useMasterKey: true }) + .then(() => q.find({ useMasterKey: true })) + .then(done.fail, done); + }); }); From b1e2594e7a1657b480202a29406137a541e8c9b4 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Tue, 24 Oct 2017 15:21:31 -0400 Subject: [PATCH 11/26] Exclude $exists query --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 791b7db605..7201d57fdc 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -665,7 +665,7 @@ function transformConstraint(constraint, field) { case '$ne': case '$eq': { const val = constraint[key]; - if (typeof val === 'object' && val.$relativeTime) { + if (key !== '$exists' && typeof val === 'object' && val.$relativeTime) { const parserResult = relativeTimeToDate(val.$relativeTime); if (parserResult.status === 'success') { answer[key] = parserResult.result; From db94a737b08f15269a479e32a0acc49b4f370345 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Tue, 24 Oct 2017 15:40:47 -0400 Subject: [PATCH 12/26] Only run integration tests on MongoDB --- spec/ParseQuery.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index e780e54d10..f3e29d6d84 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3108,7 +3108,7 @@ describe('Parse.Query testing', () => { }).catch(done.fail); }); - it('should handle relative times correctly', function(done) { + it_only_db('mongo')('should handle relative times correctly', function(done) { const now = Date.now(); const obj1 = new Parse.Object('MyCustomObject', { name: 'obj1', @@ -3148,7 +3148,7 @@ describe('Parse.Query testing', () => { .then(done, done.fail); }); - it('should error on invalid relative time', function(done) { + 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 From 5e503d16d195bf81e9cae91944db2b062b9a7fb9 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Tue, 24 Oct 2017 15:47:17 -0400 Subject: [PATCH 13/26] Add it_only_db test helper https://github.com/parse-community/parse-server/blame/bd2ea87c1d508efe337a1e8880443b1a52a8fb81/CONTRIBUTING.md#L23 --- spec/.eslintrc.json | 1 + spec/helper.js | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index 7153fae646..26d23fc755 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -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, diff --git a/spec/helper.js b/spec/helper.js index 060de28c6e..71b27157f5 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -408,6 +408,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; From 6a09f171fda85f85ce4c202eea76002e016038a1 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Tue, 24 Oct 2017 16:17:30 -0400 Subject: [PATCH 14/26] Handle where val might be null or undefined --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 7201d57fdc..91aab4ca35 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -665,7 +665,7 @@ function transformConstraint(constraint, field) { case '$ne': case '$eq': { const val = constraint[key]; - if (key !== '$exists' && typeof val === 'object' && val.$relativeTime) { + if (key !== '$exists' && val && typeof val === 'object' && val.$relativeTime) { const parserResult = relativeTimeToDate(val.$relativeTime); if (parserResult.status === 'success') { answer[key] = parserResult.result; From f8ce55613296d1bd2158c93486495920eb4b072d Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 08:37:59 -0400 Subject: [PATCH 15/26] Add integration test for multiple results --- spec/ParseQuery.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index f3e29d6d84..e3076a4c0a 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3145,6 +3145,14 @@ describe('Parse.Query testing', () => { .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); }); From 68641c4950972a725f5ec25a8313e2475c19701f Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 08:43:32 -0400 Subject: [PATCH 16/26] Lowercase text before processing --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 91aab4ca35..05cd6c916b 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -534,6 +534,8 @@ function transformTopLevelAtom(atom, field) { } function relativeTimeToDate(text, now = new Date()) { + text = text.toLowerCase(); + let parts = text.split(' '); if (!parts.length) { return { status: 'error', info: 'Not a time string' }; From f82fff2be01bac3bcd4785bac82c35dfac96affe Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 08:44:38 -0400 Subject: [PATCH 17/26] Always past if not future --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 05cd6c916b..fd402f55ef 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -561,7 +561,7 @@ function relativeTimeToDate(text, now = new Date()) { // strip the 'ago' or 'in' if (future) { parts = parts.slice(1); - } else if (past) { + } else { // past parts = parts.slice(0, parts.length - 1); } From 6a0e2ceb54eff387939614c738d08ae391e1edbd Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 08:53:39 -0400 Subject: [PATCH 18/26] Precompute seconds multiplication --- src/Adapters/Storage/Mongo/MongoTransform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index fd402f55ef..1a4ebbec85 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -590,12 +590,12 @@ function relativeTimeToDate(text, now = new Date()) { switch(interval) { case 'day': case 'days': - seconds += val * 24 * 60 * 60; + seconds += val * 86400; // 24 * 60 * 60 break; case 'hour': case 'hours': - seconds += val * 60 * 60; + seconds += val * 3600; // 60 * 60 break; case 'minute': From 50dc4ba091af3c0ce849d842d3e9a9db611b3b47 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 09:08:10 -0400 Subject: [PATCH 19/26] Add shorthand for interval hr, hrs min, mins sec, secs --- src/Adapters/Storage/Mongo/MongoTransform.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 1a4ebbec85..bd887ffb90 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -593,16 +593,22 @@ function relativeTimeToDate(text, now = new Date()) { seconds += val * 86400; // 24 * 60 * 60 break; + case 'hr': + case 'hrs': case 'hour': case 'hours': seconds += val * 3600; // 60 * 60 break; + case 'min': + case 'mins': case 'minute': case 'minutes': seconds += val * 60; break; + case 'sec': + case 'secs': case 'second': case 'seconds': seconds += val; From 5f7d26bc5c0a6d53f4fe27dd1688e090693a1d55 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 10:15:57 -0400 Subject: [PATCH 20/26] Throw error if $relativeTime is used with $exists, $ne, and $eq --- spec/ParseQuery.spec.js | 18 +++++++++++++++--- src/Adapters/Storage/Mongo/MongoTransform.js | 11 +++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index e3076a4c0a..7bae86fed8 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3119,8 +3119,7 @@ describe('Parse.Query testing', () => { ttl: new Date(now - 2 * 24 * 60 * 60 * 1000), // 2 days ago }); - dropDatabase() - .then(() => Parse.Object.saveAll([obj1, obj2])) + Parse.Object.saveAll([obj1, obj2]) .then(() => { const q = new Parse.Query('MyCustomObject'); q.greaterThan('ttl', { $relativeTime: 'in 1 day' }); @@ -3164,7 +3163,20 @@ describe('Parse.Query testing', () => { const q = new Parse.Query('MyCustomObject'); q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' }); - return obj1.save({ useMasterKey: true }) + obj1.save({ useMasterKey: true }) + .then(() => q.find({ useMasterKey: true })) + .then(done.fail, done); + }); + + it_only_db('mongo')('should error when using $relativeTime with $eq, $ne, and $', 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.exists('ttl', { $relativeTime: 'in 1 day' }); + obj1.save({ useMasterKey: true }) .then(() => q.find({ useMasterKey: true })) .then(done.fail, done); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index bd887ffb90..baa997a9b5 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -673,7 +673,14 @@ function transformConstraint(constraint, field) { case '$ne': case '$eq': { const val = constraint[key]; - if (key !== '$exists' && val && typeof val === 'object' && val.$relativeTime) { + if (val && typeof val === 'object' && val.$relativeTime) { + switch (key) { + case '$exists': + case '$ne': + case '$eq': + throw new Parse.Error(Parse.Error.INVALID_JSON, '$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'); + } + const parserResult = relativeTimeToDate(val.$relativeTime); if (parserResult.status === 'success') { answer[key] = parserResult.result; @@ -684,7 +691,7 @@ function transformConstraint(constraint, field) { throw new Parse.Error(Parse.Error.INVALID_JSON, `bad $relativeTime (${key}) value. ${parserResult.info}`); } - answer[key] = transformer(constraint[key]); + answer[key] = transformer(val); break; } From af0f83158bd1030d5176aad9b0200c848c118f4a Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 10:30:45 -0400 Subject: [PATCH 21/26] Improve coverage for relativeTimeToDate --- spec/MongoTransform.spec.js | 15 +++++++++++---- src/Adapters/Storage/Mongo/MongoTransform.js | 3 --- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 29c71651e8..7f0a119b4c 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -353,9 +353,9 @@ describe('relativeTimeToDate', () => { describe('In the future', () => { it('should parse valid natural time', () => { - const text = 'in 12 days 10 hours 24 minutes'; + 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:16.617Z'); + expect(result.toISOString()).toBe('2017-10-08T23:52:46.617Z'); expect(status).toBe('success'); expect(info).toBe('future'); }); @@ -363,15 +363,22 @@ describe('relativeTimeToDate', () => { describe('In the past', () => { it('should parse valid natural time', () => { - const text = '2 days 12 hours 1 minute ago'; + 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:16.617Z'); + 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', diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index baa997a9b5..843936be1e 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -537,9 +537,6 @@ function relativeTimeToDate(text, now = new Date()) { text = text.toLowerCase(); let parts = text.split(' '); - if (!parts.length) { - return { status: 'error', info: 'Not a time string' }; - } // Filter out whitespace parts = parts.filter((part) => part !== ''); From dc61f916332abcd5a1c09cb7419879c2a01e84e2 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 10:46:19 -0400 Subject: [PATCH 22/26] Add test for erroring on floating point units --- spec/MongoTransform.spec.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 7f0a119b4c..8d7f6c132f 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -398,6 +398,13 @@ describe('relativeTimeToDate', () => { }); }); + 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', From 19950526e1e36749ac586350dea184f0ee89c035 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 10:51:52 -0400 Subject: [PATCH 23/26] Remove unnecessary dropDatabase function --- spec/.eslintrc.json | 3 +-- spec/helper.js | 23 ----------------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index 26d23fc755..007ccf2390 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -25,8 +25,7 @@ "expectError": true, "jequal": true, "create": true, - "arrayContains": true, - "dropDatabase": true + "arrayContains": true }, "rules": { "no-console": [0] diff --git a/spec/helper.js b/spec/helper.js index 71b27157f5..f7d506361d 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -459,26 +459,3 @@ jasmine.restoreLibrary = function(library, name) { } require(library)[name] = libraryCache[library][name]; } - -const { MongoClient } = require('mongodb'); -global.dropDatabase = () => { - const connectionPromise = new Promise((resolve, reject) => - MongoClient.connect(mongoURI, (err, db) => { - if (err) { - reject(err); - } else { - resolve(db); - } - })); - - return connectionPromise.then((connection) => - new Promise((resolve, reject) => { - connection.dropDatabase((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - })); -}; From 91149e9f1bd74594198dd0268faa7eedc5d66452 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 13:30:22 -0400 Subject: [PATCH 24/26] Unit test $ne, $exists, $eq --- spec/MongoTransform.spec.js | 36 ++++++++++++++++++++ spec/ParseQuery.spec.js | 13 ------- src/Adapters/Storage/Mongo/MongoTransform.js | 1 + 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 8d7f6c132f..445b76fa1c 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -348,6 +348,42 @@ describe('transformUpdate', () => { }); }); +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: { + ttl: { + $relativeTime: '12 days ago', + } + } + }); + }).toThrow(); + }); + }) +}); + describe('relativeTimeToDate', () => { const now = new Date('2017-09-26T13:28:16.617Z'); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 7bae86fed8..e2308c1421 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3167,17 +3167,4 @@ describe('Parse.Query testing', () => { .then(() => q.find({ useMasterKey: true })) .then(done.fail, done); }); - - it_only_db('mongo')('should error when using $relativeTime with $eq, $ne, and $', 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.exists('ttl', { $relativeTime: 'in 1 day' }); - obj1.save({ useMasterKey: true }) - .then(() => q.find({ useMasterKey: true })) - .then(done.fail, done); - }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 843936be1e..983009d8fd 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -1320,4 +1320,5 @@ module.exports = { transformWhere, mongoObjectToParseObject, relativeTimeToDate, + transformConstraint, }; From a46daddf8e30f4945268144e7e7b0e36094de0fb Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 13:34:01 -0400 Subject: [PATCH 25/26] Verify field type --- spec/ParseQuery.spec.js | 14 ++++++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index e2308c1421..15884a1cd1 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3167,4 +3167,18 @@ describe('Parse.Query testing', () => { .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); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 983009d8fd..9c5c525621 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -671,6 +671,10 @@ function transformConstraint(constraint, field) { case '$eq': { const val = constraint[key]; if (val && typeof val === 'object' && val.$relativeTime) { + if (field && field.type !== 'Date') { + throw new Parse.Error(Parse.Error.INVALID_JSON, '$relativeTime can only be used with Date field'); + } + switch (key) { case '$exists': case '$ne': From 6aa7442b4751bbda7e1813b58877e8f96ff2f024 Mon Sep 17 00:00:00 2001 From: Marvel Mathew Date: Wed, 25 Oct 2017 13:39:39 -0400 Subject: [PATCH 26/26] Fix unit test for $exists Unnest query object --- spec/MongoTransform.spec.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 445b76fa1c..73c179d79f 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -374,9 +374,7 @@ describe('transformConstraint', () => { expect(() => { transform.transformConstraint({ $exists: { - ttl: { - $relativeTime: '12 days ago', - } + $relativeTime: '12 days ago', } }); }).toThrow();