Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support relativeTime query constraint on Postgres #7747

Merged
merged 6 commits into from
Jan 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions spec/MongoTransform.spec.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading