diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index f29bad99..6b6a184a 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -14,10 +14,10 @@ const masterKeyOptions = { } const loadTestData = () => { - const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, size: ['S', 'M']}; - const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, size: ['M', 'L']}; - const data3 = {score: 10, name: 'bar', sender: {group: 'B'}, size: ['S']}; - const data4 = {score: 20, name: 'dpl', sender: {group: 'B'}, size: ['S']}; + const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 900, size: ['S', 'M']}; + const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 800, size: ['M', 'L']}; + const data3 = {score: 10, name: 'bar', sender: {group: 'B'}, views: 700, size: ['S']}; + const data4 = {score: 20, name: 'dpl', sender: {group: 'B'}, views: 700, size: ['S']}; const obj1 = new TestObject(data1); const obj2 = new TestObject(data2); const obj3 = new TestObject(data3); @@ -252,7 +252,7 @@ describe('Parse.Query Aggregate testing', () => { }).catch(done.fail); }); - it('match query', (done) => { + it('match comparison query', (done) => { const options = Object.assign({}, masterKeyOptions, { body: { match: { score: { $gt: 15 }}, @@ -266,6 +266,127 @@ describe('Parse.Query Aggregate testing', () => { }).catch(done.fail); }); + it('match multiple comparison query', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + match: { score: { $gt: 5, $lt: 15 }}, + } + }); + rp.get(Parse.serverURL + '/aggregate/TestObject', options) + .then((resp) => { + expect(resp.results.length).toBe(3); + expect(resp.results[0].score).toBe(10); + expect(resp.results[1].score).toBe(10); + expect(resp.results[2].score).toBe(10); + done(); + }).catch(done.fail); + }); + + it('match complex comparison query', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 }}, + } + }); + rp.get(Parse.serverURL + '/aggregate/TestObject', options) + .then((resp) => { + expect(resp.results.length).toBe(1); + expect(resp.results[0].score).toBe(10); + expect(resp.results[0].views).toBe(900); + done(); + }).catch(done.fail); + }); + + it('match comparison and equality query', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + match: { score: { $gt: 5, $lt: 15 }, views: 900}, + } + }); + rp.get(Parse.serverURL + '/aggregate/TestObject', options) + .then((resp) => { + expect(resp.results.length).toBe(1); + expect(resp.results[0].score).toBe(10); + expect(resp.results[0].views).toBe(900); + done(); + }).catch(done.fail); + }); + + it('match $or query', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + match: { $or: [{ score: { $gt: 15, $lt: 25 } }, { views: { $gt: 750, $lt: 850 } }]}, + } + }); + rp.get(Parse.serverURL + '/aggregate/TestObject', options) + .then((resp) => { + expect(resp.results.length).toBe(2); + // Match score { $gt: 15, $lt: 25 } + expect(resp.results.some(result => result.score === 20)).toEqual(true); + expect(resp.results.some(result => result.views === 700)).toEqual(true); + + // Match view { $gt: 750, $lt: 850 } + expect(resp.results.some(result => result.score === 10)).toEqual(true); + expect(resp.results.some(result => result.views === 800)).toEqual(true); + done(); + }).catch(done.fail); + }); + + it('match objectId query', (done) => { + const obj1 = new TestObject(); + const obj2 = new TestObject(); + Parse.Object.saveAll([obj1, obj2]).then(() => { + const pipeline = [ + { match: { objectId: obj1.id } } + ]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }).then((results) => { + expect(results.length).toEqual(1); + expect(results[0].objectId).toEqual(obj1.id); + done(); + }); + }); + + it('match field query', (done) => { + const obj1 = new TestObject({ name: 'TestObject1'}); + const obj2 = new TestObject({ name: 'TestObject2'}); + Parse.Object.saveAll([obj1, obj2]).then(() => { + const pipeline = [ + { match: { name: 'TestObject1' } } + ]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }).then((results) => { + expect(results.length).toEqual(1); + expect(results[0].objectId).toEqual(obj1.id); + done(); + }); + }); + + it('match pointer query', (done) => { + const pointer1 = new TestObject(); + const pointer2 = new TestObject(); + const obj1 = new TestObject({ pointer: pointer1 }); + const obj2 = new TestObject({ pointer: pointer2 }); + const obj3 = new TestObject({ pointer: pointer1 }); + + Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]).then(() => { + const pipeline = [ + { match: { pointer: pointer1.id } } + ]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }).then((results) => { + expect(results.length).toEqual(2); + expect(results[0].pointer.objectId).toEqual(pointer1.id); + expect(results[1].pointer.objectId).toEqual(pointer1.id); + expect(results.some(result => result.objectId === obj1.id)).toEqual(true); + expect(results.some(result => result.objectId === obj3.id)).toEqual(true); + done(); + }); + }); + it('project query', (done) => { const options = Object.assign({}, masterKeyOptions, { body: { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index b5d97c2d..5e217349 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -524,6 +524,20 @@ export class MongoStorageAdapter implements StorageAdapter { stage.$group._id = `$_p_${field}`; } } + if (stage.$match) { + for (const field in stage.$match) { + if (schema.fields[field] && schema.fields[field].type === 'Pointer') { + const transformMatch = { [`_p_${field}`] : `${className}$${stage.$match[field]}` }; + stage.$match = transformMatch; + } + if (field === 'objectId') { + const transformMatch = Object.assign({}, stage.$match); + transformMatch._id = stage.$match[field]; + delete transformMatch.objectId; + stage.$match = transformMatch; + } + } + } return stage; }); readPreference = this._parseReadPreference(readPreference); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 601ac645..90614751 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1507,6 +1507,7 @@ export class PostgresStorageAdapter implements StorageAdapter { aggregate(className: string, schema: any, pipeline: any) { debug('aggregate', className, pipeline); const values = [className]; + let index = 2; let columns: string[] = []; let countField = null; let wherePattern = ''; @@ -1523,26 +1524,38 @@ export class PostgresStorageAdapter implements StorageAdapter { continue; } if (field === '_id') { - columns.push(`${transformAggregateField(value)} AS "objectId"`); - groupPattern = `GROUP BY ${transformAggregateField(value)}`; + columns.push(`$${index}:name AS "objectId"`); + groupPattern = `GROUP BY $${index}:name`; + values.push(transformAggregateField(value)); + index += 1; continue; } if (value.$sum) { if (typeof value.$sum === 'string') { - columns.push(`SUM(${transformAggregateField(value.$sum)}) AS "${field}"`); + columns.push(`SUM($${index}:name) AS $${index + 1}:name`); + values.push(transformAggregateField(value.$sum), field); + index += 2; } else { countField = field; - columns.push(`COUNT(*) AS "${field}"`); + columns.push(`COUNT(*) AS $${index}:name`); + values.push(field); + index += 1; } } if (value.$max) { - columns.push(`MAX(${transformAggregateField(value.$max)}) AS "${field}"`); + columns.push(`MAX($${index}:name) AS $${index + 1}:name`); + values.push(transformAggregateField(value.$max), field); + index += 2; } if (value.$min) { - columns.push(`MIN(${transformAggregateField(value.$min)}) AS "${field}"`); + columns.push(`MIN($${index}:name) AS $${index + 1}:name`); + values.push(transformAggregateField(value.$min), field); + index += 2; } if (value.$avg) { - columns.push(`AVG(${transformAggregateField(value.$avg)}) AS "${field}"`); + columns.push(`AVG($${index}:name) AS $${index + 1}:name`); + values.push(transformAggregateField(value.$avg), field); + index += 2; } } } else { @@ -1555,38 +1568,68 @@ export class PostgresStorageAdapter implements StorageAdapter { for (const field in stage.$project) { const value = stage.$project[field]; if ((value === 1 || value === true)) { - columns.push(field); + columns.push(`$${index}:name`); + values.push(field); + index += 1; } } } 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]}`); + const orOrAnd = stage.$match.hasOwnProperty('$or') ? ' OR ' : ' AND '; + + if (stage.$match.$or) { + const collapse = {}; + stage.$match.$or.forEach((element) => { + for (const key in element) { + collapse[key] = element[key]; } }); + stage.$match = collapse; } - wherePattern = patterns.length > 0 ? `WHERE ${patterns.join(' ')}` : ''; + for (const field in stage.$match) { + const value = stage.$match[field]; + const matchPatterns = []; + Object.keys(ParseToPosgresComparator).forEach((cmp) => { + if (value[cmp]) { + const pgComparator = ParseToPosgresComparator[cmp]; + matchPatterns.push(`$${index}:name ${pgComparator} $${index + 1}`); + values.push(field, toPostgresValue(value[cmp])); + index += 2; + } + }); + if (matchPatterns.length > 0) { + patterns.push(`(${matchPatterns.join(' AND ')})`); + } + if (schema.fields[field] && schema.fields[field].type && matchPatterns.length === 0) { + patterns.push(`$${index}:name = $${index + 1}`); + values.push(field, value); + index += 2; + } + } + wherePattern = patterns.length > 0 ? `WHERE ${patterns.join(` ${orOrAnd} `)}` : ''; } if (stage.$limit) { - limitPattern = `LIMIT ${stage.$limit}`; + limitPattern = `LIMIT $${index}`; + values.push(stage.$limit); + index += 1; } if (stage.$skip) { - skipPattern = `OFFSET ${stage.$skip}`; + skipPattern = `OFFSET $${index}`; + values.push(stage.$skip); + index += 1; } if (stage.$sort) { const sort = stage.$sort; - const sorting = Object.keys(sort).map((key) => { - if (sort[key] === 1) { - return `"${key}" ASC`; - } - return `"${key}" DESC`; + const keys = Object.keys(sort); + const sorting = keys.map((key) => { + const transformer = sort[key] === 1 ? 'ASC' : 'DESC'; + const order = `$${index}:name ${transformer}`; + index += 1; + return order; }).join(); - sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : ''; + values.push(...keys); + sortPattern = sort !== undefined && sorting.length > 0 ? `ORDER BY ${sorting}` : ''; } }