-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Support for Aggregate Queries #4207
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
Changes from all commits
9ef32c7
a138d86
a04ede9
8b5e858
ea3ec59
6b7b2ca
a8d1b24
9c866c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -165,6 +165,10 @@ const transformDotField = (fieldName) => { | |
return name; | ||
} | ||
|
||
const transformAggregateField = (fieldName) => { | ||
return fieldName.substr(1); | ||
} | ||
|
||
const validateKeys = (object) => { | ||
if (typeof object == 'object') { | ||
for (const key in object) { | ||
|
@@ -1366,6 +1370,140 @@ export class PostgresStorageAdapter { | |
}); | ||
} | ||
|
||
distinct(className, schema, query, fieldName) { | ||
debug('distinct', className, query); | ||
let field = fieldName; | ||
let column = fieldName; | ||
if (fieldName.indexOf('.') >= 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although this will work the value won't ever be 0, just -1 or > 1. Doesn't mean this needs to be changed, but it could also be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It will never happen but it covers all cases There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright. |
||
field = transformDotFieldToComponents(fieldName).join('->'); | ||
column = fieldName.split('.')[0]; | ||
} | ||
const isArrayField = schema.fields | ||
&& schema.fields[fieldName] | ||
&& schema.fields[fieldName].type === 'Array'; | ||
const values = [field, column, className]; | ||
const where = buildWhereClause({ schema, query, index: 4 }); | ||
values.push(...where.values); | ||
|
||
const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; | ||
let qs = `SELECT DISTINCT ON ($1:raw) $2:raw FROM $3:name ${wherePattern}`; | ||
if (isArrayField) { | ||
qs = `SELECT distinct jsonb_array_elements($1:raw) as $2:raw FROM $3:name ${wherePattern}`; | ||
} | ||
debug(qs, values); | ||
return this._client.any(qs, values) | ||
.catch(() => []) | ||
.then((results) => { | ||
if (fieldName.indexOf('.') === -1) { | ||
return results.map(object => object[field]); | ||
} | ||
const child = fieldName.split('.')[1]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I pass a field name like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Additionally something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, just covering bases. Took a look, this is good too. |
||
return results.map(object => object[column][child]); | ||
}); | ||
} | ||
|
||
aggregate(className, pipeline) { | ||
debug('aggregate', className, pipeline); | ||
const values = [className]; | ||
let columns = []; | ||
let countField = null; | ||
let wherePattern = ''; | ||
let limitPattern = ''; | ||
let skipPattern = ''; | ||
let sortPattern = ''; | ||
let groupPattern = ''; | ||
for (let i = 0; i < pipeline.length; i += 1) { | ||
const stage = pipeline[i]; | ||
if (stage.$group) { | ||
for (const field in stage.$group) { | ||
const value = stage.$group[field]; | ||
if (value === null || value === undefined) { | ||
continue; | ||
} | ||
if (field === '_id') { | ||
columns.push(`${transformAggregateField(value)} AS "objectId"`); | ||
groupPattern = `GROUP BY ${transformAggregateField(value)}`; | ||
continue; | ||
} | ||
if (value.$sum) { | ||
if (typeof value.$sum === 'string') { | ||
columns.push(`SUM(${transformAggregateField(value.$sum)}) AS "${field}"`); | ||
} else { | ||
countField = field; | ||
columns.push(`COUNT(*) AS "${field}"`); | ||
} | ||
} | ||
if (value.$max) { | ||
columns.push(`MAX(${transformAggregateField(value.$max)}) AS "${field}"`); | ||
} | ||
if (value.$min) { | ||
columns.push(`MIN(${transformAggregateField(value.$min)}) AS "${field}"`); | ||
} | ||
if (value.$avg) { | ||
columns.push(`AVG(${transformAggregateField(value.$avg)}) AS "${field}"`); | ||
} | ||
} | ||
columns.join(','); | ||
} else { | ||
columns.push('*'); | ||
} | ||
if (stage.$project) { | ||
if (columns.includes('*')) { | ||
columns = []; | ||
} | ||
for (const field in stage.$project) { | ||
const value = stage.$project[field]; | ||
if ((value === 1 || value === true)) { | ||
columns.push(field); | ||
} | ||
} | ||
} | ||
if (stage.$match) { | ||
const patterns = []; | ||
for (const field in stage.$match) { | ||
const value = stage.$match[field]; | ||
Object.keys(ParseToPosgresComparator).forEach(cmp => { | ||
if (value[cmp]) { | ||
const pgComparator = ParseToPosgresComparator[cmp]; | ||
patterns.push(`${field} ${pgComparator} ${value[cmp]}`); | ||
} | ||
}); | ||
} | ||
wherePattern = patterns.length > 0 ? `WHERE ${patterns.join(' ')}` : ''; | ||
} | ||
if (stage.$limit) { | ||
limitPattern = `LIMIT ${stage.$limit}`; | ||
} | ||
if (stage.$skip) { | ||
skipPattern = `OFFSET ${stage.$skip}`; | ||
} | ||
if (stage.$sort) { | ||
const sort = stage.$sort; | ||
const sorting = Object.keys(sort).map((key) => { | ||
if (sort[key] === 1) { | ||
return `"${key}" ASC`; | ||
} | ||
return `"${key}" DESC`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps a bit silly, but you could mark sort with a value of |
||
}).join(','); | ||
sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : ''; | ||
} | ||
} | ||
|
||
const qs = `SELECT ${columns} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern} ${groupPattern}`; | ||
debug(qs, values); | ||
return this._client.any(qs, values).then(results => { | ||
if (countField) { | ||
results[0][countField] = parseInt(results[0][countField], 10); | ||
} | ||
results.forEach(result => { | ||
if (!result.hasOwnProperty('objectId')) { | ||
result.objectId = null; | ||
} | ||
}); | ||
return results; | ||
}); | ||
} | ||
|
||
performInitialization({ VolatileClassesSchemas }) { | ||
debug('performInitialization'); | ||
const promises = VolatileClassesSchemas.map((schema) => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import ClassesRouter from './ClassesRouter'; | ||
import rest from '../rest'; | ||
import * as middleware from '../middlewares'; | ||
import Parse from 'parse/node'; | ||
|
||
const ALLOWED_KEYS = [ | ||
'where', | ||
'distinct', | ||
'project', | ||
'match', | ||
'redact', | ||
'limit', | ||
'skip', | ||
'unwind', | ||
'group', | ||
'sample', | ||
'sort', | ||
'geoNear', | ||
'lookup', | ||
'out', | ||
'indexStats', | ||
'facet', | ||
'bucket', | ||
'bucketAuto', | ||
'sortByCount', | ||
'addFields', | ||
'replaceRoot', | ||
'count', | ||
'graphLookup', | ||
]; | ||
|
||
export class AggregateRouter extends ClassesRouter { | ||
|
||
handleFind(req) { | ||
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); | ||
const options = {}; | ||
const pipeline = []; | ||
|
||
for (const key in body) { | ||
if (ALLOWED_KEYS.indexOf(key) === -1) { | ||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid parameter for query: ${key}`); | ||
} | ||
if (key === 'group') { | ||
if (body[key].hasOwnProperty('_id')) { | ||
throw new Parse.Error( | ||
Parse.Error.INVALID_QUERY, | ||
`Invalid parameter for query: group. Please use objectId instead of _id` | ||
); | ||
} | ||
if (!body[key].hasOwnProperty('objectId')) { | ||
throw new Parse.Error( | ||
Parse.Error.INVALID_QUERY, | ||
`Invalid parameter for query: group. objectId is required` | ||
); | ||
} | ||
body[key]._id = body[key].objectId; | ||
delete body[key].objectId; | ||
} | ||
pipeline.push({ [`$${key}`]: body[key] }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. $$? Double checking that this isn't just a working typo... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scratch this, you're good here as well 👍 |
||
} | ||
if (body.distinct) { | ||
options.distinct = String(body.distinct); | ||
} | ||
options.pipeline = pipeline; | ||
if (typeof body.where === 'string') { | ||
body.where = JSON.parse(body.where); | ||
} | ||
return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK) | ||
.then((response) => { return { response }; }); | ||
} | ||
|
||
mountRoutes() { | ||
this.route('GET','/aggregate/:className', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleFind(req); }); | ||
} | ||
} | ||
|
||
export default AggregateRouter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious what's theNvm, logger.debug
mark here for?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing
PARSE_SERVER_LOG_LEVEL=debug
It really helps for testing PG