feat: support relativeTime query constraint on Postgres (#7747)

This commit is contained in:
Corey
2022-01-01 19:10:54 -05:00
committed by GitHub
parent 7448521cb9
commit 16b1b2a197
6 changed files with 366 additions and 217 deletions

View File

@@ -2,6 +2,7 @@ import log from '../../../logger';
import _ from 'lodash';
var mongodb = require('mongodb');
var Parse = require('parse/node').Parse;
const Utils = require('../../../Utils');
const transformKey = (className, fieldName, schema) => {
// 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.
// A constraint is something with fields like $lt.
// 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') {
answer[key] = parserResult.result;
break;
@@ -1556,7 +1430,6 @@ module.exports = {
transformUpdate,
transformWhere,
mongoObjectToParseObject,
relativeTimeToDate,
transformConstraint,
transformPointerString,
};

View File

@@ -7,6 +7,9 @@ import _ from 'lodash';
// @flow-disable-next
import { v4 as uuidv4 } from 'uuid';
import sql from './sql';
import { StorageAdapter } from '../StorageAdapter';
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
const Utils = require('../../../Utils');
const PostgresRelationDoesNotExistError = '42P01';
const PostgresDuplicateRelationError = '42P07';
@@ -22,9 +25,6 @@ const debug = function (...args: any) {
log.debug.apply(log, args);
};
import { StorageAdapter } from '../StorageAdapter';
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
const parseTypeToPostgresType = type => {
switch (type.type) {
case 'String':
@@ -374,6 +374,11 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
patterns.push(
`(${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 {
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) {
values.push(fieldValue.$eq);
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 {
values.push(fieldName, fieldValue.$eq);
patterns.push(`$${index}:name = $${index + 1}`);
@@ -513,7 +523,12 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
}
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`);
} else {
patterns.push(`$${index}:name IS NULL`);
@@ -757,7 +772,7 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
Object.keys(ParseToPosgresComparator).forEach(cmp => {
if (fieldValue[cmp] || fieldValue[cmp] === 0) {
const pgComparator = ParseToPosgresComparator[cmp];
const postgresValue = toPostgresValue(fieldValue[cmp]);
let postgresValue = toPostgresValue(fieldValue[cmp]);
let constraintFieldName;
if (fieldName.indexOf('.') >= 0) {
let castType;
@@ -775,6 +790,24 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
: transformDotField(fieldName);
} 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`;
values.push(fieldName);
}