Skip to content

Commit

Permalink
feat: support relativeTime query constraint on Postgres (#7747)
Browse files Browse the repository at this point in the history
  • Loading branch information
cbaker6 committed Jan 2, 2022
1 parent 7448521 commit 16b1b2a
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 217 deletions.
23 changes: 12 additions & 11 deletions spec/MongoTransform.spec.js
Expand Up @@ -4,6 +4,7 @@
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
const dd = require('deep-diff');
const mongodb = require('mongodb');
const Utils = require('../lib/Utils');

describe('parseObjectToMongoObjectForCreate', () => {
it('a basic number', done => {
Expand Down Expand Up @@ -592,7 +593,7 @@ describe('relativeTimeToDate', () => {
describe('In the future', () => {
it('should parse valid natural time', () => {
const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds';
const { result, status, info } = transform.relativeTimeToDate(text, now);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z');
expect(status).toBe('success');
expect(info).toBe('future');
Expand All @@ -602,7 +603,7 @@ describe('relativeTimeToDate', () => {
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);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
expect(status).toBe('success');
expect(info).toBe('past');
Expand All @@ -612,7 +613,7 @@ describe('relativeTimeToDate', () => {
describe('From now', () => {
it('should equal current time', () => {
const text = 'now';
const { result, status, info } = transform.relativeTimeToDate(text, now);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z');
expect(status).toBe('success');
expect(info).toBe('present');
Expand All @@ -621,54 +622,54 @@ describe('relativeTimeToDate', () => {

describe('Error cases', () => {
it('should error if string is completely gibberish', () => {
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
expect(Utils.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({
expect(Utils.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({
expect(Utils.relativeTimeToDate('in 12 hours 1')).toEqual({
status: 'error',
info: 'Invalid time string. Dangling unit or number.',
});

expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({
expect(Utils.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({
expect(Utils.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({
expect(Utils.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({
expect(Utils.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({
expect(Utils.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
status: 'error',
info: "Time cannot have both 'in' and 'ago'",
});
Expand Down
125 changes: 53 additions & 72 deletions spec/ParseQuery.spec.js
Expand Up @@ -4766,7 +4766,7 @@ describe('Parse.Query testing', () => {
.catch(done.fail);
});

it_only_db('mongo')('should handle relative times correctly', function (done) {
it('should handle relative times correctly', async () => {
const now = Date.now();
const obj1 = new Parse.Object('MyCustomObject', {
name: 'obj1',
Expand All @@ -4777,94 +4777,75 @@ describe('Parse.Query testing', () => {
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(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: 'now' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(1);
})
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: 'now' });
q.lessThan('ttl', { $relativeTime: 'in 1 day' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(0);
})
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(2);
})
.then(done, done.fail);
await Parse.Object.saveAll([obj1, obj2])
const q1 = new Parse.Query('MyCustomObject');
q1.greaterThan('ttl', { $relativeTime: 'in 1 day' });
const results1 = await q1.find({ useMasterKey: true });
expect(results1.length).toBe(1);

const q2 = new Parse.Query('MyCustomObject');
q2.greaterThan('ttl', { $relativeTime: '1 day ago' });
const results2 = await q2.find({ useMasterKey: true });
expect(results2.length).toBe(1);

const q3 = new Parse.Query('MyCustomObject');
q3.lessThan('ttl', { $relativeTime: '5 days ago' });
const results3 = await q3.find({ useMasterKey: true });
expect(results3.length).toBe(0);

const q4 = new Parse.Query('MyCustomObject');
q4.greaterThan('ttl', { $relativeTime: '3 days ago' });
const results4 = await q4.find({ useMasterKey: true });
expect(results4.length).toBe(2);

const q5 = new Parse.Query('MyCustomObject');
q5.greaterThan('ttl', { $relativeTime: 'now' });
const results5 = await q5.find({ useMasterKey: true });
expect(results5.length).toBe(1);

const q6 = new Parse.Query('MyCustomObject');
q6.greaterThan('ttl', { $relativeTime: 'now' });
q6.lessThan('ttl', { $relativeTime: 'in 1 day' });
const results6 = await q6.find({ useMasterKey: true });
expect(results6.length).toBe(0);

const q7 = new Parse.Query('MyCustomObject');
q7.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
const results7 = await q7.find({ useMasterKey: true });
expect(results7.length).toBe(2);
});

it_only_db('mongo')('should error on invalid relative time', function (done) {
it('should error on invalid relative time', async () => {
const obj1 = new Parse.Object('MyCustomObject', {
name: 'obj1',
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
});

await obj1.save({ useMasterKey: true });
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());
try {
await q.find({ useMasterKey: true });
fail("Should have thrown error");
} catch(error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
});

it_only_db('mongo')('should error when using $relativeTime on non-Date field', function (done) {
it('should error when using $relativeTime on non-Date field', async () => {
const obj1 = new Parse.Object('MyCustomObject', {
name: 'obj1',
nonDateField: 'abcd',
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
});

await obj1.save({ useMasterKey: true });
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());
try {
await q.find({ useMasterKey: true });
fail("Should have thrown error");
} catch(error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
});

it('should match complex structure with dot notation when using matchesKeyInQuery', function (done) {
Expand Down
129 changes: 129 additions & 0 deletions spec/PostgresStorageAdapter.spec.js
Expand Up @@ -149,6 +149,135 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
});

it('$relativeTime should error on $eq', async () => {
const tableName = '_User';
const schema = {
fields: {
objectId: { type: 'String' },
username: { type: 'String' },
email: { type: 'String' },
emailVerified: { type: 'Boolean' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
authData: { type: 'Object' },
},
};
const client = adapter._client;
await adapter.createTable(tableName, schema);
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
tableName,
'objectId',
'username',
'Bugs',
'Bunny',
]);
const database = Config.get(Parse.applicationId).database;
await database.loadSchema({ clearCache: true });
try {
await database.find(
tableName,
{
createdAt: {
$eq: {
$relativeTime: '12 days ago'
}
}
},
{ }
);
fail("Should have thrown error");
} catch(error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
await dropTable(client, tableName);
});

it('$relativeTime should error on $ne', async () => {
const tableName = '_User';
const schema = {
fields: {
objectId: { type: 'String' },
username: { type: 'String' },
email: { type: 'String' },
emailVerified: { type: 'Boolean' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
authData: { type: 'Object' },
},
};
const client = adapter._client;
await adapter.createTable(tableName, schema);
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
tableName,
'objectId',
'username',
'Bugs',
'Bunny',
]);
const database = Config.get(Parse.applicationId).database;
await database.loadSchema({ clearCache: true });
try {
await database.find(
tableName,
{
createdAt: {
$ne: {
$relativeTime: '12 days ago'
}
}
},
{ }
);
fail("Should have thrown error");
} catch(error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
await dropTable(client, tableName);
});

it('$relativeTime should error on $exists', async () => {
const tableName = '_User';
const schema = {
fields: {
objectId: { type: 'String' },
username: { type: 'String' },
email: { type: 'String' },
emailVerified: { type: 'Boolean' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
authData: { type: 'Object' },
},
};
const client = adapter._client;
await adapter.createTable(tableName, schema);
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
tableName,
'objectId',
'username',
'Bugs',
'Bunny',
]);
const database = Config.get(Parse.applicationId).database;
await database.loadSchema({ clearCache: true });
try {
await database.find(
tableName,
{
createdAt: {
$exists: {
$relativeTime: '12 days ago'
}
}
},
{ }
);
fail("Should have thrown error");
} catch(error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
await dropTable(client, tableName);
});

it('should use index for caseInsensitive query using Postgres', async () => {
const tableName = '_User';
const schema = {
Expand Down

0 comments on commit 16b1b2a

Please sign in to comment.