Skip to content

Commit 73f26c3

Browse files
committed
feat: add relativeTime query to Postgres
1 parent 191d80b commit 73f26c3

File tree

3 files changed

+170
-8
lines changed

3 files changed

+170
-8
lines changed

spec/ParseQuery.spec.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -4766,7 +4766,7 @@ describe('Parse.Query testing', () => {
47664766
.catch(done.fail);
47674767
});
47684768

4769-
it_only_db('mongo')('should handle relative times correctly', function (done) {
4769+
it('should handle relative times correctly', function (done) {
47704770
const now = Date.now();
47714771
const obj1 = new Parse.Object('MyCustomObject', {
47724772
name: 'obj1',
@@ -4838,7 +4838,7 @@ describe('Parse.Query testing', () => {
48384838
.then(done, done.fail);
48394839
});
48404840

4841-
it_only_db('mongo')('should error on invalid relative time', function (done) {
4841+
it('should error on invalid relative time', function (done) {
48424842
const obj1 = new Parse.Object('MyCustomObject', {
48434843
name: 'obj1',
48444844
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
@@ -4852,7 +4852,7 @@ describe('Parse.Query testing', () => {
48524852
.then(done.fail, () => done());
48534853
});
48544854

4855-
it_only_db('mongo')('should error when using $relativeTime on non-Date field', function (done) {
4855+
it('should error when using $relativeTime on non-Date field', function (done) {
48564856
const obj1 = new Parse.Object('MyCustomObject', {
48574857
name: 'obj1',
48584858
nonDateField: 'abcd',

spec/PostgresStorageAdapter.spec.js

+129
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,135 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
149149
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
150150
});
151151

152+
it('$relativeTime should error on $eq', async () => {
153+
const tableName = '_User';
154+
const schema = {
155+
fields: {
156+
objectId: { type: 'String' },
157+
username: { type: 'String' },
158+
email: { type: 'String' },
159+
emailVerified: { type: 'Boolean' },
160+
createdAt: { type: 'Date' },
161+
updatedAt: { type: 'Date' },
162+
authData: { type: 'Object' },
163+
},
164+
};
165+
const client = adapter._client;
166+
await adapter.createTable(tableName, schema);
167+
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
168+
tableName,
169+
'objectId',
170+
'username',
171+
'Bugs',
172+
'Bunny',
173+
]);
174+
const database = Config.get(Parse.applicationId).database;
175+
await database.loadSchema({ clearCache: true });
176+
try {
177+
await database.find(
178+
tableName,
179+
{
180+
createdAt: {
181+
$eq: {
182+
$relativeTime: '12 days ago'
183+
}
184+
}
185+
},
186+
{ }
187+
);
188+
fail("Should have thrown error");
189+
} catch(error) {
190+
expect(error.code).toBe(Parse.Error.INVALID_JSON);
191+
}
192+
await dropTable(client, tableName);
193+
});
194+
195+
it('$relativeTime should error on $ne', async () => {
196+
const tableName = '_User';
197+
const schema = {
198+
fields: {
199+
objectId: { type: 'String' },
200+
username: { type: 'String' },
201+
email: { type: 'String' },
202+
emailVerified: { type: 'Boolean' },
203+
createdAt: { type: 'Date' },
204+
updatedAt: { type: 'Date' },
205+
authData: { type: 'Object' },
206+
},
207+
};
208+
const client = adapter._client;
209+
await adapter.createTable(tableName, schema);
210+
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
211+
tableName,
212+
'objectId',
213+
'username',
214+
'Bugs',
215+
'Bunny',
216+
]);
217+
const database = Config.get(Parse.applicationId).database;
218+
await database.loadSchema({ clearCache: true });
219+
try {
220+
await database.find(
221+
tableName,
222+
{
223+
createdAt: {
224+
$ne: {
225+
$relativeTime: '12 days ago'
226+
}
227+
}
228+
},
229+
{ }
230+
);
231+
fail("Should have thrown error");
232+
} catch(error) {
233+
expect(error.code).toBe(Parse.Error.INVALID_JSON);
234+
}
235+
await dropTable(client, tableName);
236+
});
237+
238+
it('$relativeTime should error on $exists', async () => {
239+
const tableName = '_User';
240+
const schema = {
241+
fields: {
242+
objectId: { type: 'String' },
243+
username: { type: 'String' },
244+
email: { type: 'String' },
245+
emailVerified: { type: 'Boolean' },
246+
createdAt: { type: 'Date' },
247+
updatedAt: { type: 'Date' },
248+
authData: { type: 'Object' },
249+
},
250+
};
251+
const client = adapter._client;
252+
await adapter.createTable(tableName, schema);
253+
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
254+
tableName,
255+
'objectId',
256+
'username',
257+
'Bugs',
258+
'Bunny',
259+
]);
260+
const database = Config.get(Parse.applicationId).database;
261+
await database.loadSchema({ clearCache: true });
262+
try {
263+
await database.find(
264+
tableName,
265+
{
266+
createdAt: {
267+
$exists: {
268+
$relativeTime: '12 days ago'
269+
}
270+
}
271+
},
272+
{ }
273+
);
274+
fail("Should have thrown error");
275+
} catch(error) {
276+
expect(error.code).toBe(Parse.Error.INVALID_JSON);
277+
}
278+
await dropTable(client, tableName);
279+
});
280+
152281
it('should use index for caseInsensitive query using Postgres', async () => {
153282
const tableName = '_User';
154283
const schema = {

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

+38-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import _ from 'lodash';
77
// @flow-disable-next
88
import { v4 as uuidv4 } from 'uuid';
99
import sql from './sql';
10+
import { StorageAdapter } from '../StorageAdapter';
11+
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
12+
import { relativeTimeToDate } from '../Mongo/MongoTransform';
1013

1114
const PostgresRelationDoesNotExistError = '42P01';
1215
const PostgresDuplicateRelationError = '42P07';
@@ -22,9 +25,6 @@ const debug = function (...args: any) {
2225
log.debug.apply(log, args);
2326
};
2427

25-
import { StorageAdapter } from '../StorageAdapter';
26-
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
27-
2828
const parseTypeToPostgresType = type => {
2929
switch (type.type) {
3030
case 'String':
@@ -374,6 +374,11 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
374374
patterns.push(
375375
`(${constraintFieldName} <> $${index} OR ${constraintFieldName} IS NULL)`
376376
);
377+
} else if (typeof fieldValue.$ne === 'object' && fieldValue.$ne.$relativeTime) {
378+
throw new Parse.Error(
379+
Parse.Error.INVALID_JSON,
380+
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
381+
);
377382
} else {
378383
patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`);
379384
}
@@ -399,6 +404,11 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
399404
if (fieldName.indexOf('.') >= 0) {
400405
values.push(fieldValue.$eq);
401406
patterns.push(`${transformDotField(fieldName)} = $${index++}`);
407+
} if (typeof fieldValue.$eq === 'object' && fieldValue.$eq.$relativeTime) {
408+
throw new Parse.Error(
409+
Parse.Error.INVALID_JSON,
410+
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
411+
);
402412
} else {
403413
values.push(fieldName, fieldValue.$eq);
404414
patterns.push(`$${index}:name = $${index + 1}`);
@@ -513,7 +523,12 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
513523
}
514524

515525
if (typeof fieldValue.$exists !== 'undefined') {
516-
if (fieldValue.$exists) {
526+
if (typeof fieldValue.$exists === 'object' && fieldValue.$exists.$relativeTime) {
527+
throw new Parse.Error(
528+
Parse.Error.INVALID_JSON,
529+
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
530+
);
531+
} else if (fieldValue.$exists) {
517532
patterns.push(`$${index}:name IS NOT NULL`);
518533
} else {
519534
patterns.push(`$${index}:name IS NULL`);
@@ -757,7 +772,7 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
757772
Object.keys(ParseToPosgresComparator).forEach(cmp => {
758773
if (fieldValue[cmp] || fieldValue[cmp] === 0) {
759774
const pgComparator = ParseToPosgresComparator[cmp];
760-
const postgresValue = toPostgresValue(fieldValue[cmp]);
775+
let postgresValue = toPostgresValue(fieldValue[cmp]);
761776
let constraintFieldName;
762777
if (fieldName.indexOf('.') >= 0) {
763778
let castType;
@@ -775,6 +790,24 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
775790
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
776791
: transformDotField(fieldName);
777792
} else {
793+
if (typeof postgresValue === 'object' && postgresValue.$relativeTime) {
794+
if (schema.fields[fieldName].type !== 'Date') {
795+
throw new Parse.Error(
796+
Parse.Error.INVALID_JSON,
797+
'$relativeTime can only be used with Date field'
798+
);
799+
}
800+
const parserResult = relativeTimeToDate(postgresValue.$relativeTime);
801+
if (parserResult.status === 'success') {
802+
postgresValue = toPostgresValue(parserResult.result);
803+
} else {
804+
console.error('Error while parsing relative date', parserResult);
805+
throw new Parse.Error(
806+
Parse.Error.INVALID_JSON,
807+
`bad $relativeTime (${key}) value. ${parserResult.info}`
808+
);
809+
}
810+
}
778811
constraintFieldName = `$${index++}:name`;
779812
values.push(fieldName);
780813
}

0 commit comments

Comments
 (0)