feat: support relativeTime query constraint on Postgres (#7747)
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
|
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
|
||||||
const dd = require('deep-diff');
|
const dd = require('deep-diff');
|
||||||
const mongodb = require('mongodb');
|
const mongodb = require('mongodb');
|
||||||
|
const Utils = require('../lib/Utils');
|
||||||
|
|
||||||
describe('parseObjectToMongoObjectForCreate', () => {
|
describe('parseObjectToMongoObjectForCreate', () => {
|
||||||
it('a basic number', done => {
|
it('a basic number', done => {
|
||||||
@@ -592,7 +593,7 @@ describe('relativeTimeToDate', () => {
|
|||||||
describe('In the future', () => {
|
describe('In the future', () => {
|
||||||
it('should parse valid natural time', () => {
|
it('should parse valid natural time', () => {
|
||||||
const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds';
|
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(result.toISOString()).toBe('2018-10-22T23:52:46.617Z');
|
||||||
expect(status).toBe('success');
|
expect(status).toBe('success');
|
||||||
expect(info).toBe('future');
|
expect(info).toBe('future');
|
||||||
@@ -602,7 +603,7 @@ describe('relativeTimeToDate', () => {
|
|||||||
describe('In the past', () => {
|
describe('In the past', () => {
|
||||||
it('should parse valid natural time', () => {
|
it('should parse valid natural time', () => {
|
||||||
const text = '2 days 12 hours 1 minute 12 seconds ago';
|
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(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
|
||||||
expect(status).toBe('success');
|
expect(status).toBe('success');
|
||||||
expect(info).toBe('past');
|
expect(info).toBe('past');
|
||||||
@@ -612,7 +613,7 @@ describe('relativeTimeToDate', () => {
|
|||||||
describe('From now', () => {
|
describe('From now', () => {
|
||||||
it('should equal current time', () => {
|
it('should equal current time', () => {
|
||||||
const text = 'now';
|
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(result.toISOString()).toBe('2017-09-26T13:28:16.617Z');
|
||||||
expect(status).toBe('success');
|
expect(status).toBe('success');
|
||||||
expect(info).toBe('present');
|
expect(info).toBe('present');
|
||||||
@@ -621,54 +622,54 @@ describe('relativeTimeToDate', () => {
|
|||||||
|
|
||||||
describe('Error cases', () => {
|
describe('Error cases', () => {
|
||||||
it('should error if string is completely gibberish', () => {
|
it('should error if string is completely gibberish', () => {
|
||||||
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
|
expect(Utils.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
info: "Time should either start with 'in' or end with 'ago'",
|
info: "Time should either start with 'in' or end with 'ago'",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if string contains neither `ago` nor `in`', () => {
|
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',
|
status: 'error',
|
||||||
info: "Time should either start with 'in' or end with 'ago'",
|
info: "Time should either start with 'in' or end with 'ago'",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if there are missing units or numbers', () => {
|
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',
|
status: 'error',
|
||||||
info: 'Invalid time string. Dangling unit or number.',
|
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',
|
status: 'error',
|
||||||
info: 'Invalid time string. Dangling unit or number.',
|
info: 'Invalid time string. Dangling unit or number.',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error on floating point numbers', () => {
|
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',
|
status: 'error',
|
||||||
info: "'12.3' is not an integer.",
|
info: "'12.3' is not an integer.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if numbers are invalid', () => {
|
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',
|
status: 'error',
|
||||||
info: "'123a' is not an integer.",
|
info: "'123a' is not an integer.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error on invalid interval units', () => {
|
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',
|
status: 'error',
|
||||||
info: "Invalid interval: 'score'",
|
info: "Invalid interval: 'score'",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should error when string contains 'ago' and 'in'", () => {
|
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',
|
status: 'error',
|
||||||
info: "Time cannot have both 'in' and 'ago'",
|
info: "Time cannot have both 'in' and 'ago'",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4766,7 +4766,7 @@ describe('Parse.Query testing', () => {
|
|||||||
.catch(done.fail);
|
.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 now = Date.now();
|
||||||
const obj1 = new Parse.Object('MyCustomObject', {
|
const obj1 = new Parse.Object('MyCustomObject', {
|
||||||
name: 'obj1',
|
name: 'obj1',
|
||||||
@@ -4777,94 +4777,75 @@ describe('Parse.Query testing', () => {
|
|||||||
ttl: new Date(now - 2 * 24 * 60 * 60 * 1000), // 2 days ago
|
ttl: new Date(now - 2 * 24 * 60 * 60 * 1000), // 2 days ago
|
||||||
});
|
});
|
||||||
|
|
||||||
Parse.Object.saveAll([obj1, obj2])
|
await Parse.Object.saveAll([obj1, obj2])
|
||||||
.then(() => {
|
const q1 = new Parse.Query('MyCustomObject');
|
||||||
const q = new Parse.Query('MyCustomObject');
|
q1.greaterThan('ttl', { $relativeTime: 'in 1 day' });
|
||||||
q.greaterThan('ttl', { $relativeTime: 'in 1 day' });
|
const results1 = await q1.find({ useMasterKey: true });
|
||||||
return q.find({ useMasterKey: true });
|
expect(results1.length).toBe(1);
|
||||||
})
|
|
||||||
.then(results => {
|
const q2 = new Parse.Query('MyCustomObject');
|
||||||
expect(results.length).toBe(1);
|
q2.greaterThan('ttl', { $relativeTime: '1 day ago' });
|
||||||
})
|
const results2 = await q2.find({ useMasterKey: true });
|
||||||
.then(() => {
|
expect(results2.length).toBe(1);
|
||||||
const q = new Parse.Query('MyCustomObject');
|
|
||||||
q.greaterThan('ttl', { $relativeTime: '1 day ago' });
|
const q3 = new Parse.Query('MyCustomObject');
|
||||||
return q.find({ useMasterKey: true });
|
q3.lessThan('ttl', { $relativeTime: '5 days ago' });
|
||||||
})
|
const results3 = await q3.find({ useMasterKey: true });
|
||||||
.then(results => {
|
expect(results3.length).toBe(0);
|
||||||
expect(results.length).toBe(1);
|
|
||||||
})
|
const q4 = new Parse.Query('MyCustomObject');
|
||||||
.then(() => {
|
q4.greaterThan('ttl', { $relativeTime: '3 days ago' });
|
||||||
const q = new Parse.Query('MyCustomObject');
|
const results4 = await q4.find({ useMasterKey: true });
|
||||||
q.lessThan('ttl', { $relativeTime: '5 days ago' });
|
expect(results4.length).toBe(2);
|
||||||
return q.find({ useMasterKey: true });
|
|
||||||
})
|
const q5 = new Parse.Query('MyCustomObject');
|
||||||
.then(results => {
|
q5.greaterThan('ttl', { $relativeTime: 'now' });
|
||||||
expect(results.length).toBe(0);
|
const results5 = await q5.find({ useMasterKey: true });
|
||||||
})
|
expect(results5.length).toBe(1);
|
||||||
.then(() => {
|
|
||||||
const q = new Parse.Query('MyCustomObject');
|
const q6 = new Parse.Query('MyCustomObject');
|
||||||
q.greaterThan('ttl', { $relativeTime: '3 days ago' });
|
q6.greaterThan('ttl', { $relativeTime: 'now' });
|
||||||
return q.find({ useMasterKey: true });
|
q6.lessThan('ttl', { $relativeTime: 'in 1 day' });
|
||||||
})
|
const results6 = await q6.find({ useMasterKey: true });
|
||||||
.then(results => {
|
expect(results6.length).toBe(0);
|
||||||
expect(results.length).toBe(2);
|
|
||||||
})
|
const q7 = new Parse.Query('MyCustomObject');
|
||||||
.then(() => {
|
q7.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
|
||||||
const q = new Parse.Query('MyCustomObject');
|
const results7 = await q7.find({ useMasterKey: true });
|
||||||
q.greaterThan('ttl', { $relativeTime: 'now' });
|
expect(results7.length).toBe(2);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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', {
|
const obj1 = new Parse.Object('MyCustomObject', {
|
||||||
name: 'obj1',
|
name: 'obj1',
|
||||||
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
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');
|
const q = new Parse.Query('MyCustomObject');
|
||||||
q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' });
|
q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' });
|
||||||
obj1
|
try {
|
||||||
.save({ useMasterKey: true })
|
await q.find({ useMasterKey: true });
|
||||||
.then(() => q.find({ useMasterKey: true }))
|
fail("Should have thrown error");
|
||||||
.then(done.fail, () => done());
|
} 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', {
|
const obj1 = new Parse.Object('MyCustomObject', {
|
||||||
name: 'obj1',
|
name: 'obj1',
|
||||||
nonDateField: 'abcd',
|
nonDateField: 'abcd',
|
||||||
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
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');
|
const q = new Parse.Query('MyCustomObject');
|
||||||
q.greaterThan('nonDateField', { $relativeTime: '1 day ago' });
|
q.greaterThan('nonDateField', { $relativeTime: '1 day ago' });
|
||||||
obj1
|
try {
|
||||||
.save({ useMasterKey: true })
|
await q.find({ useMasterKey: true });
|
||||||
.then(() => q.find({ useMasterKey: true }))
|
fail("Should have thrown error");
|
||||||
.then(done.fail, () => done());
|
} catch(error) {
|
||||||
|
expect(error.code).toBe(Parse.Error.INVALID_JSON);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match complex structure with dot notation when using matchesKeyInQuery', function (done) {
|
it('should match complex structure with dot notation when using matchesKeyInQuery', function (done) {
|
||||||
|
|||||||
@@ -149,6 +149,135 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
|
|||||||
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
|
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 () => {
|
it('should use index for caseInsensitive query using Postgres', async () => {
|
||||||
const tableName = '_User';
|
const tableName = '_User';
|
||||||
const schema = {
|
const schema = {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import log from '../../../logger';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
var mongodb = require('mongodb');
|
var mongodb = require('mongodb');
|
||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
|
const Utils = require('../../../Utils');
|
||||||
|
|
||||||
const transformKey = (className, fieldName, schema) => {
|
const transformKey = (className, fieldName, schema) => {
|
||||||
// Check if the schema is known since it's a built-in field.
|
// Check if the schema is known since it's a built-in field.
|
||||||
@@ -634,133 +635,6 @@ function transformTopLevelAtom(atom, field) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function relativeTimeToDate(text, now = new Date()) {
|
|
||||||
text = text.toLowerCase();
|
|
||||||
|
|
||||||
let parts = text.split(' ');
|
|
||||||
|
|
||||||
// Filter out whitespace
|
|
||||||
parts = parts.filter(part => part !== '');
|
|
||||||
|
|
||||||
const future = parts[0] === 'in';
|
|
||||||
const past = parts[parts.length - 1] === 'ago';
|
|
||||||
|
|
||||||
if (!future && !past && text !== 'now') {
|
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
// past
|
|
||||||
parts = parts.slice(0, parts.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length % 2 !== 0 && text !== 'now') {
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
info: 'Invalid time string. Dangling unit or number.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const pairs = [];
|
|
||||||
while (parts.length) {
|
|
||||||
pairs.push([parts.shift(), parts.shift()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 'yr':
|
|
||||||
case 'yrs':
|
|
||||||
case 'year':
|
|
||||||
case 'years':
|
|
||||||
seconds += val * 31536000; // 365 * 24 * 60 * 60
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'wk':
|
|
||||||
case 'wks':
|
|
||||||
case 'week':
|
|
||||||
case 'weeks':
|
|
||||||
seconds += val * 604800; // 7 * 24 * 60 * 60
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'd':
|
|
||||||
case 'day':
|
|
||||||
case 'days':
|
|
||||||
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;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
info: `Invalid interval: '${interval}'`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const milliseconds = seconds * 1000;
|
|
||||||
if (future) {
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
info: 'future',
|
|
||||||
result: new Date(now.valueOf() + milliseconds),
|
|
||||||
};
|
|
||||||
} else if (past) {
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
info: 'past',
|
|
||||||
result: new Date(now.valueOf() - milliseconds),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
info: 'present',
|
|
||||||
result: new Date(now.valueOf()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transforms a query constraint from REST API format to Mongo format.
|
// Transforms a query constraint from REST API format to Mongo format.
|
||||||
// A constraint is something with fields like $lt.
|
// A constraint is something with fields like $lt.
|
||||||
// If it is not a valid constraint but it could be a valid something
|
// If it is not a valid constraint but it could be a valid something
|
||||||
@@ -813,7 +687,7 @@ function transformConstraint(constraint, field, count = false) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parserResult = relativeTimeToDate(val.$relativeTime);
|
const parserResult = Utils.relativeTimeToDate(val.$relativeTime);
|
||||||
if (parserResult.status === 'success') {
|
if (parserResult.status === 'success') {
|
||||||
answer[key] = parserResult.result;
|
answer[key] = parserResult.result;
|
||||||
break;
|
break;
|
||||||
@@ -1556,7 +1430,6 @@ module.exports = {
|
|||||||
transformUpdate,
|
transformUpdate,
|
||||||
transformWhere,
|
transformWhere,
|
||||||
mongoObjectToParseObject,
|
mongoObjectToParseObject,
|
||||||
relativeTimeToDate,
|
|
||||||
transformConstraint,
|
transformConstraint,
|
||||||
transformPointerString,
|
transformPointerString,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import _ from 'lodash';
|
|||||||
// @flow-disable-next
|
// @flow-disable-next
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import sql from './sql';
|
import sql from './sql';
|
||||||
|
import { StorageAdapter } from '../StorageAdapter';
|
||||||
|
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
|
||||||
|
const Utils = require('../../../Utils');
|
||||||
|
|
||||||
const PostgresRelationDoesNotExistError = '42P01';
|
const PostgresRelationDoesNotExistError = '42P01';
|
||||||
const PostgresDuplicateRelationError = '42P07';
|
const PostgresDuplicateRelationError = '42P07';
|
||||||
@@ -22,9 +25,6 @@ const debug = function (...args: any) {
|
|||||||
log.debug.apply(log, args);
|
log.debug.apply(log, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
import { StorageAdapter } from '../StorageAdapter';
|
|
||||||
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
|
|
||||||
|
|
||||||
const parseTypeToPostgresType = type => {
|
const parseTypeToPostgresType = type => {
|
||||||
switch (type.type) {
|
switch (type.type) {
|
||||||
case 'String':
|
case 'String':
|
||||||
@@ -374,6 +374,11 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
|||||||
patterns.push(
|
patterns.push(
|
||||||
`(${constraintFieldName} <> $${index} OR ${constraintFieldName} IS NULL)`
|
`(${constraintFieldName} <> $${index} OR ${constraintFieldName} IS NULL)`
|
||||||
);
|
);
|
||||||
|
} else if (typeof fieldValue.$ne === 'object' && fieldValue.$ne.$relativeTime) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`);
|
patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`);
|
||||||
}
|
}
|
||||||
@@ -399,6 +404,11 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
|||||||
if (fieldName.indexOf('.') >= 0) {
|
if (fieldName.indexOf('.') >= 0) {
|
||||||
values.push(fieldValue.$eq);
|
values.push(fieldValue.$eq);
|
||||||
patterns.push(`${transformDotField(fieldName)} = $${index++}`);
|
patterns.push(`${transformDotField(fieldName)} = $${index++}`);
|
||||||
|
} else if (typeof fieldValue.$eq === 'object' && fieldValue.$eq.$relativeTime) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
values.push(fieldName, fieldValue.$eq);
|
values.push(fieldName, fieldValue.$eq);
|
||||||
patterns.push(`$${index}:name = $${index + 1}`);
|
patterns.push(`$${index}:name = $${index + 1}`);
|
||||||
@@ -513,7 +523,12 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof fieldValue.$exists !== 'undefined') {
|
if (typeof fieldValue.$exists !== 'undefined') {
|
||||||
if (fieldValue.$exists) {
|
if (typeof fieldValue.$exists === 'object' && fieldValue.$exists.$relativeTime) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
|
||||||
|
);
|
||||||
|
} else if (fieldValue.$exists) {
|
||||||
patterns.push(`$${index}:name IS NOT NULL`);
|
patterns.push(`$${index}:name IS NOT NULL`);
|
||||||
} else {
|
} else {
|
||||||
patterns.push(`$${index}:name IS NULL`);
|
patterns.push(`$${index}:name IS NULL`);
|
||||||
@@ -757,7 +772,7 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
|||||||
Object.keys(ParseToPosgresComparator).forEach(cmp => {
|
Object.keys(ParseToPosgresComparator).forEach(cmp => {
|
||||||
if (fieldValue[cmp] || fieldValue[cmp] === 0) {
|
if (fieldValue[cmp] || fieldValue[cmp] === 0) {
|
||||||
const pgComparator = ParseToPosgresComparator[cmp];
|
const pgComparator = ParseToPosgresComparator[cmp];
|
||||||
const postgresValue = toPostgresValue(fieldValue[cmp]);
|
let postgresValue = toPostgresValue(fieldValue[cmp]);
|
||||||
let constraintFieldName;
|
let constraintFieldName;
|
||||||
if (fieldName.indexOf('.') >= 0) {
|
if (fieldName.indexOf('.') >= 0) {
|
||||||
let castType;
|
let castType;
|
||||||
@@ -775,6 +790,24 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
|||||||
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
|
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
|
||||||
: transformDotField(fieldName);
|
: transformDotField(fieldName);
|
||||||
} else {
|
} else {
|
||||||
|
if (typeof postgresValue === 'object' && postgresValue.$relativeTime) {
|
||||||
|
if (schema.fields[fieldName].type !== 'Date') {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
'$relativeTime can only be used with Date field'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const parserResult = Utils.relativeTimeToDate(postgresValue.$relativeTime);
|
||||||
|
if (parserResult.status === 'success') {
|
||||||
|
postgresValue = toPostgresValue(parserResult.result);
|
||||||
|
} else {
|
||||||
|
console.error('Error while parsing relative date', parserResult);
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
`bad $relativeTime (${postgresValue.$relativeTime}) value. ${parserResult.info}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
constraintFieldName = `$${index++}:name`;
|
constraintFieldName = `$${index++}:name`;
|
||||||
values.push(fieldName);
|
values.push(fieldName);
|
||||||
}
|
}
|
||||||
|
|||||||
132
src/Utils.js
132
src/Utils.js
@@ -200,6 +200,138 @@ class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the relative date based on a string.
|
||||||
|
* @param {String} text The string to interpret the date from.
|
||||||
|
* @param {Date} now The date the string is comparing against.
|
||||||
|
* @returns {Object} The relative date object.
|
||||||
|
**/
|
||||||
|
static relativeTimeToDate(text, now = new Date()) {
|
||||||
|
text = text.toLowerCase();
|
||||||
|
let parts = text.split(' ');
|
||||||
|
|
||||||
|
// Filter out whitespace
|
||||||
|
parts = parts.filter(part => part !== '');
|
||||||
|
|
||||||
|
const future = parts[0] === 'in';
|
||||||
|
const past = parts[parts.length - 1] === 'ago';
|
||||||
|
|
||||||
|
if (!future && !past && text !== 'now') {
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
// past
|
||||||
|
parts = parts.slice(0, parts.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length % 2 !== 0 && text !== 'now') {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
info: 'Invalid time string. Dangling unit or number.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pairs = [];
|
||||||
|
while (parts.length) {
|
||||||
|
pairs.push([parts.shift(), parts.shift()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 'yr':
|
||||||
|
case 'yrs':
|
||||||
|
case 'year':
|
||||||
|
case 'years':
|
||||||
|
seconds += val * 31536000; // 365 * 24 * 60 * 60
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'wk':
|
||||||
|
case 'wks':
|
||||||
|
case 'week':
|
||||||
|
case 'weeks':
|
||||||
|
seconds += val * 604800; // 7 * 24 * 60 * 60
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'd':
|
||||||
|
case 'day':
|
||||||
|
case 'days':
|
||||||
|
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;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
info: `Invalid interval: '${interval}'`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const milliseconds = seconds * 1000;
|
||||||
|
if (future) {
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
info: 'future',
|
||||||
|
result: new Date(now.valueOf() + milliseconds),
|
||||||
|
};
|
||||||
|
} else if (past) {
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
info: 'past',
|
||||||
|
result: new Date(now.valueOf() - milliseconds),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
info: 'present',
|
||||||
|
result: new Date(now.valueOf()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Utils;
|
module.exports = Utils;
|
||||||
|
|||||||
Reference in New Issue
Block a user