From c3ef4f44e2f34fafd614ba67828c4840144f427d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:07:57 +0200 Subject: [PATCH 01/13] feat: Add `rawValues` per-query option with EJSON input deserialization for aggregation --- spec/ParseQuery.Aggregate.spec.js | 14 +++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 24 ++++++++++++------- .../Postgres/PostgresStorageAdapter.js | 3 ++- src/Adapters/Storage/StorageAdapter.js | 3 ++- src/Controllers/DatabaseController.js | 4 +++- src/RestQuery.js | 1 + src/Routers/AggregateRouter.js | 4 ++++ 7 files changed, 42 insertions(+), 11 deletions(-) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 1085589fb4..f67f49dd56 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -473,6 +473,20 @@ describe('Parse.Query Aggregate testing', () => { expect(new Date(results[0].date.iso)).toEqual(obj1.get('date')); }); + it_id('f01a0001-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('rawValues: true converts $date EJSON marker to BSON Date in $match', async () => { + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + const pipeline = [ + { $match: { objectId: obj.id, createdAt: { $lte: { $date: iso } } } }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true }); + expect(results.length).toBe(1); + expect(results[0].total).toBe(1); + }); + it_only_db('postgres')( 'can group by any date field postgres (it does not work if you have dirty data)', // rows in your collection with non date data in the field that is supposed to be a date done => { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 151264703e..1ab70b0c88 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -17,6 +17,7 @@ import { import Parse from 'parse/node'; // @flow-disable-next import _ from 'lodash'; +import { EJSON } from 'bson'; import defaults, { ParseServerDatabaseOptions } from '../../../defaults'; import logger from '../../../logger'; @@ -936,9 +937,13 @@ export class MongoStorageAdapter implements StorageAdapter { readPreference: ?string, hint: ?mixed, explain?: boolean, - comment: ?string + comment: ?string, + rawValues?: boolean ) { validateExplainValue(explain); + if (rawValues) { + pipeline = EJSON.deserialize(pipeline); + } let isPointerField = false; pipeline = pipeline.map(stage => { if (stage.$group) { @@ -952,13 +957,13 @@ export class MongoStorageAdapter implements StorageAdapter { } } if (stage.$match) { - stage.$match = this._parseAggregateArgs(schema, stage.$match); + stage.$match = this._parseAggregateArgs(schema, stage.$match, rawValues); } if (stage.$project) { stage.$project = this._parseAggregateProjectArgs(schema, stage.$project); } if (stage.$geoNear && stage.$geoNear.query) { - stage.$geoNear.query = this._parseAggregateArgs(schema, stage.$geoNear.query); + stage.$geoNear.query = this._parseAggregateArgs(schema, stage.$geoNear.query, rawValues); } return stage; }); @@ -1016,25 +1021,28 @@ export class MongoStorageAdapter implements StorageAdapter { // // As much as I hate recursion...this seemed like a good fit for it. We're essentially traversing // down a tree to find a "leaf node" and checking to see if it needs to be converted. - _parseAggregateArgs(schema: any, pipeline: any): any { + _parseAggregateArgs(schema: any, pipeline: any, rawValues?: boolean): any { if (pipeline === null) { return null; + } else if (Utils.isDate(pipeline)) { + return pipeline; } else if (Array.isArray(pipeline)) { - return pipeline.map(value => this._parseAggregateArgs(schema, value)); + return pipeline.map(value => this._parseAggregateArgs(schema, value, rawValues)); } else if (typeof pipeline === 'object') { const returnValue = {}; for (const field in pipeline) { if (schema.fields[field] && schema.fields[field].type === 'Pointer') { if (typeof pipeline[field] === 'object') { - // Pass objects down to MongoDB...this is more than likely an $exists operator. + returnValue[`_p_${field}`] = pipeline[field]; + } else if (rawValues) { returnValue[`_p_${field}`] = pipeline[field]; } else { returnValue[`_p_${field}`] = `${schema.fields[field].targetClass}$${pipeline[field]}`; } - } else if (schema.fields[field] && schema.fields[field].type === 'Date') { + } else if (schema.fields[field] && schema.fields[field].type === 'Date' && !rawValues) { returnValue[field] = this._convertToDate(pipeline[field]); } else { - returnValue[field] = this._parseAggregateArgs(schema, pipeline[field]); + returnValue[field] = this._parseAggregateArgs(schema, pipeline[field], rawValues); } if (field === 'objectId') { diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 08f8c647f4..0ea3d2cf04 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -2271,7 +2271,8 @@ export class PostgresStorageAdapter implements StorageAdapter { pipeline: any, readPreference: ?string, hint: ?mixed, - explain?: boolean + explain?: boolean, + _rawValues?: boolean ) { debug('aggregate'); const values = [className]; diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index 49e1c23d36..2a74f9b86d 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -125,7 +125,8 @@ export interface StorageAdapter { readPreference: ?string, hint: ?mixed, explain?: boolean, - comment?: string + comment?: string, + rawValues?: boolean ): Promise; performInitialization(options: ?any): Promise; watch(callback: () => void): void; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 4180893c58..5989ad7b03 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1269,6 +1269,7 @@ class DatabaseController { caseInsensitive = false, explain, comment, + rawValues, }: any = {}, auth: any = {}, validSchemaController: SchemaController.SchemaController @@ -1409,7 +1410,8 @@ class DatabaseController { readPreference, hint, explain, - comment + comment, + rawValues ); } } else if (explain) { diff --git a/src/RestQuery.js b/src/RestQuery.js index 912c328a66..064b6fb353 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -219,6 +219,7 @@ function _UnsafeRestQuery( case 'limit': case 'readPreference': case 'comment': + case 'rawValues': this.findOptions[option] = restOptions[option]; break; case 'order': diff --git a/src/Routers/AggregateRouter.js b/src/Routers/AggregateRouter.js index 5b35a9fbb9..846f4987e8 100644 --- a/src/Routers/AggregateRouter.js +++ b/src/Routers/AggregateRouter.js @@ -27,6 +27,10 @@ export class AggregateRouter extends ClassesRouter { options.readPreference = body.readPreference; delete body.readPreference; } + if (typeof body.rawValues === 'boolean') { + options.rawValues = body.rawValues; + delete body.rawValues; + } options.pipeline = AggregateRouter.getPipeline(body); if (typeof body.where === 'string') { try { From 3acabcc67b349239d827bfba369a83bed2c9453d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:09:55 +0200 Subject: [PATCH 02/13] fix: Add missing `_comment` parameter to Postgres aggregate signature for positional alignment --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 0ea3d2cf04..9343c78b81 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -2272,6 +2272,7 @@ export class PostgresStorageAdapter implements StorageAdapter { readPreference: ?string, hint: ?mixed, explain?: boolean, + _comment?: ?string, _rawValues?: boolean ) { debug('aggregate'); From 8c63f79d812432b96c590b5d7d5a8ca5a75475c5 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:11:20 +0200 Subject: [PATCH 03/13] test: Add rawValues input-side coverage for nesting and non-EJSON values --- spec/ParseQuery.Aggregate.spec.js | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index f67f49dd56..08a2c290a8 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -487,6 +487,60 @@ describe('Parse.Query Aggregate testing', () => { expect(results[0].total).toBe(1); }); + it_id('f01a0001-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('rawValues: true deserializes $date at any nesting depth', async () => { + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + const pipeline = [ + { + $match: { + $and: [ + { objectId: obj.id }, + { $or: [{ createdAt: { $lte: { $date: iso } } }] }, + ], + }, + }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true }); + expect(results.length).toBe(1); + expect(results[0].total).toBe(1); + }); + + it_id('f01a0001-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('rawValues: true does NOT coerce bare ISO strings', async () => { + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + const pipeline = [ + { $match: { objectId: obj.id, createdAt: { $lte: iso } } }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true }); + // Bare ISO string compared against BSON Date: MongoDB string-vs-date comparison yields no matches. + expect(results.length).toBe(0); + }); + + it_id('f01a0001-0004-0004-0004-000000000004')(it_exclude_dbs(['postgres']))('rawValues: true does NOT coerce Parse Date encoding `{ __type: "Date", iso }`', async () => { + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + const pipeline = [ + { + $match: { + objectId: obj.id, + createdAt: { $lte: { __type: 'Date', iso } }, + }, + }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true }); + // Parse Date encoding is not interpreted in rawValues mode; comparison fails silently. + expect(results.length).toBe(0); + }); + it_only_db('postgres')( 'can group by any date field postgres (it does not work if you have dirty data)', // rows in your collection with non date data in the field that is supposed to be a date done => { From fcd49ae8a37293867e339823c9f47e5c041d39c4 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:44:34 +0200 Subject: [PATCH 04/13] feat: Serialize aggregation results via EJSON when rawValues is true --- spec/ParseQuery.Aggregate.spec.js | 15 +++++++++++++++ src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 10 +++++++++- src/Routers/AggregateRouter.js | 8 +++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 08a2c290a8..ef3ca5a799 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -541,6 +541,21 @@ describe('Parse.Query Aggregate testing', () => { expect(results.length).toBe(0); }); + it_id('f01a0001-0005-0005-0005-000000000005')(it_exclude_dbs(['postgres']))('rawValues: true serializes BSON Date in results as `{ $date: iso }`', async () => { + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + const pipeline = [ + { $match: { objectId: obj.id, createdAt: { $lte: { $date: iso } } } }, + { $project: { _id: 1, _created_at: 1 } }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true }); + expect(results.length).toBe(1); + // EJSON-serialized date marker, not Parse `{ __type: 'Date', iso }` encoding. + expect(results[0]._created_at).toEqual(jasmine.objectContaining({ $date: jasmine.any(String) })); + }); + it_only_db('postgres')( 'can group by any date field postgres (it does not work if you have dirty data)', // rows in your collection with non date data in the field that is supposed to be a date done => { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 1ab70b0c88..12a5e2defd 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -980,6 +980,9 @@ export class MongoStorageAdapter implements StorageAdapter { }) ) .then(results => { + if (rawValues) { + return results; + } results.forEach(result => { if (Object.prototype.hasOwnProperty.call(result, '_id')) { if (isPointerField && result._id) { @@ -998,7 +1001,12 @@ export class MongoStorageAdapter implements StorageAdapter { }); return results; }) - .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) + .then(objects => { + if (rawValues) { + return objects.map(obj => EJSON.serialize(obj)); + } + return objects.map(object => mongoObjectToParseObject(className, object, schema)); + }) .catch(err => this.handleError(err)); } diff --git a/src/Routers/AggregateRouter.js b/src/Routers/AggregateRouter.js index 846f4987e8..ce569fd709 100644 --- a/src/Routers/AggregateRouter.js +++ b/src/Routers/AggregateRouter.js @@ -49,9 +49,11 @@ export class AggregateRouter extends ClassesRouter { req.info.clientSDK, req.info.context ); - for (const result of response.results) { - if (typeof result === 'object') { - UsersRouter.removeHiddenProperties(result); + if (!options.rawValues) { + for (const result of response.results) { + if (typeof result === 'object') { + UsersRouter.removeHiddenProperties(result); + } } } return { response }; From 5d4a3181a7f6c63545bbef9b285063d87efb04ac Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:47:22 +0200 Subject: [PATCH 05/13] test: Add rawValues coverage for `$addFields` stage --- spec/ParseQuery.Aggregate.spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index ef3ca5a799..8e2318779b 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -556,6 +556,21 @@ describe('Parse.Query Aggregate testing', () => { expect(results[0]._created_at).toEqual(jasmine.objectContaining({ $date: jasmine.any(String) })); }); + it_id('f01a0001-0006-0006-0006-000000000006')(it_exclude_dbs(['postgres']))('rawValues: true deserializes EJSON in `$addFields`', async () => { + const obj = new TestObject(); + await obj.save(); + const iso = '2026-01-01T00:00:00.000Z'; + const pipeline = [ + { $match: { objectId: obj.id } }, + { $addFields: { pinned: { $date: iso } } }, + { $project: { _id: 1, pinned: 1 } }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { rawValues: true, useMasterKey: true }); + expect(results.length).toBe(1); + expect(results[0].pinned).toEqual(jasmine.objectContaining({ $date: jasmine.any(String) })); + }); + it_only_db('postgres')( 'can group by any date field postgres (it does not work if you have dirty data)', // rows in your collection with non date data in the field that is supposed to be a date done => { From 40587d7bf78ae8bd285ff2cceab8de86f6d51610 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:56:18 +0200 Subject: [PATCH 06/13] feat: Add `rawFieldNames` per-query option for aggregation pipeline --- spec/ParseQuery.Aggregate.spec.js | 23 ++++++ .../Storage/Mongo/MongoStorageAdapter.js | 78 ++++++++++--------- .../Postgres/PostgresStorageAdapter.js | 3 +- src/Adapters/Storage/StorageAdapter.js | 3 +- src/Controllers/DatabaseController.js | 4 +- src/RestQuery.js | 1 + src/Routers/AggregateRouter.js | 6 +- 7 files changed, 79 insertions(+), 39 deletions(-) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 8e2318779b..7e799fe3b9 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -1652,4 +1652,27 @@ describe('Parse.Query Aggregate testing', () => { expect(e.code).toBe(Parse.Error.INVALID_QUERY); } }); + + it_id('f01a0002-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('rawFieldNames: true lets users write _created_at directly', async () => { + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + const pipeline = [ + { + $match: { + _id: obj.id, + _created_at: { $lte: { $date: iso } }, + }, + }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { + rawValues: true, + rawFieldNames: true, + useMasterKey: true, + }); + expect(results.length).toBe(1); + expect(results[0].total).toBe(1); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 12a5e2defd..9c17b2a18b 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -938,7 +938,8 @@ export class MongoStorageAdapter implements StorageAdapter { hint: ?mixed, explain?: boolean, comment: ?string, - rawValues?: boolean + rawValues?: boolean, + rawFieldNames?: boolean ) { validateExplainValue(explain); if (rawValues) { @@ -947,7 +948,7 @@ export class MongoStorageAdapter implements StorageAdapter { let isPointerField = false; pipeline = pipeline.map(stage => { if (stage.$group) { - stage.$group = this._parseAggregateGroupArgs(schema, stage.$group); + stage.$group = this._parseAggregateGroupArgs(schema, stage.$group, rawFieldNames); if ( stage.$group._id && typeof stage.$group._id === 'string' && @@ -957,13 +958,13 @@ export class MongoStorageAdapter implements StorageAdapter { } } if (stage.$match) { - stage.$match = this._parseAggregateArgs(schema, stage.$match, rawValues); + stage.$match = this._parseAggregateArgs(schema, stage.$match, rawValues, rawFieldNames); } if (stage.$project) { - stage.$project = this._parseAggregateProjectArgs(schema, stage.$project); + stage.$project = this._parseAggregateProjectArgs(schema, stage.$project, rawValues, rawFieldNames); } if (stage.$geoNear && stage.$geoNear.query) { - stage.$geoNear.query = this._parseAggregateArgs(schema, stage.$geoNear.query, rawValues); + stage.$geoNear.query = this._parseAggregateArgs(schema, stage.$geoNear.query, rawValues, rawFieldNames); } return stage; }); @@ -980,7 +981,7 @@ export class MongoStorageAdapter implements StorageAdapter { }) ) .then(results => { - if (rawValues) { + if (rawFieldNames) { return results; } results.forEach(result => { @@ -1005,6 +1006,9 @@ export class MongoStorageAdapter implements StorageAdapter { if (rawValues) { return objects.map(obj => EJSON.serialize(obj)); } + if (rawFieldNames) { + return objects; + } return objects.map(object => mongoObjectToParseObject(className, object, schema)); }) .catch(err => this.handleError(err)); @@ -1029,17 +1033,17 @@ export class MongoStorageAdapter implements StorageAdapter { // // As much as I hate recursion...this seemed like a good fit for it. We're essentially traversing // down a tree to find a "leaf node" and checking to see if it needs to be converted. - _parseAggregateArgs(schema: any, pipeline: any, rawValues?: boolean): any { + _parseAggregateArgs(schema: any, pipeline: any, rawValues?: boolean, rawFieldNames?: boolean): any { if (pipeline === null) { return null; } else if (Utils.isDate(pipeline)) { return pipeline; } else if (Array.isArray(pipeline)) { - return pipeline.map(value => this._parseAggregateArgs(schema, value, rawValues)); + return pipeline.map(value => this._parseAggregateArgs(schema, value, rawValues, rawFieldNames)); } else if (typeof pipeline === 'object') { const returnValue = {}; for (const field in pipeline) { - if (schema.fields[field] && schema.fields[field].type === 'Pointer') { + if (!rawFieldNames && schema.fields[field] && schema.fields[field].type === 'Pointer') { if (typeof pipeline[field] === 'object') { returnValue[`_p_${field}`] = pipeline[field]; } else if (rawValues) { @@ -1050,18 +1054,20 @@ export class MongoStorageAdapter implements StorageAdapter { } else if (schema.fields[field] && schema.fields[field].type === 'Date' && !rawValues) { returnValue[field] = this._convertToDate(pipeline[field]); } else { - returnValue[field] = this._parseAggregateArgs(schema, pipeline[field], rawValues); + returnValue[field] = this._parseAggregateArgs(schema, pipeline[field], rawValues, rawFieldNames); } - if (field === 'objectId') { - returnValue['_id'] = returnValue[field]; - delete returnValue[field]; - } else if (field === 'createdAt') { - returnValue['_created_at'] = returnValue[field]; - delete returnValue[field]; - } else if (field === 'updatedAt') { - returnValue['_updated_at'] = returnValue[field]; - delete returnValue[field]; + if (!rawFieldNames) { + if (field === 'objectId') { + returnValue['_id'] = returnValue[field]; + delete returnValue[field]; + } else if (field === 'createdAt') { + returnValue['_created_at'] = returnValue[field]; + delete returnValue[field]; + } else if (field === 'updatedAt') { + returnValue['_updated_at'] = returnValue[field]; + delete returnValue[field]; + } } } return returnValue; @@ -1073,24 +1079,26 @@ export class MongoStorageAdapter implements StorageAdapter { // two functions and making the code even harder to understand, I decided to split it up. The // difference with this function is we are not transforming the values, only the keys of the // pipeline. - _parseAggregateProjectArgs(schema: any, pipeline: any): any { + _parseAggregateProjectArgs(schema: any, pipeline: any, rawValues?: boolean, rawFieldNames?: boolean): any { const returnValue = {}; for (const field in pipeline) { - if (schema.fields[field] && schema.fields[field].type === 'Pointer') { + if (!rawFieldNames && schema.fields[field] && schema.fields[field].type === 'Pointer') { returnValue[`_p_${field}`] = pipeline[field]; } else { - returnValue[field] = this._parseAggregateArgs(schema, pipeline[field]); + returnValue[field] = this._parseAggregateArgs(schema, pipeline[field], rawValues, rawFieldNames); } - if (field === 'objectId') { - returnValue['_id'] = returnValue[field]; - delete returnValue[field]; - } else if (field === 'createdAt') { - returnValue['_created_at'] = returnValue[field]; - delete returnValue[field]; - } else if (field === 'updatedAt') { - returnValue['_updated_at'] = returnValue[field]; - delete returnValue[field]; + if (!rawFieldNames) { + if (field === 'objectId') { + returnValue['_id'] = returnValue[field]; + delete returnValue[field]; + } else if (field === 'createdAt') { + returnValue['_created_at'] = returnValue[field]; + delete returnValue[field]; + } else if (field === 'updatedAt') { + returnValue['_updated_at'] = returnValue[field]; + delete returnValue[field]; + } } } return returnValue; @@ -1101,16 +1109,16 @@ export class MongoStorageAdapter implements StorageAdapter { // The could be a column name, prefixed with the '$' character. We'll look for // these and check to see if it is a 'Pointer' or if it's one of createdAt, // updatedAt or objectId and change it accordingly. - _parseAggregateGroupArgs(schema: any, pipeline: any): any { + _parseAggregateGroupArgs(schema: any, pipeline: any, rawFieldNames?: boolean): any { if (Array.isArray(pipeline)) { - return pipeline.map(value => this._parseAggregateGroupArgs(schema, value)); + return pipeline.map(value => this._parseAggregateGroupArgs(schema, value, rawFieldNames)); } else if (typeof pipeline === 'object') { const returnValue = {}; for (const field in pipeline) { - returnValue[field] = this._parseAggregateGroupArgs(schema, pipeline[field]); + returnValue[field] = this._parseAggregateGroupArgs(schema, pipeline[field], rawFieldNames); } return returnValue; - } else if (typeof pipeline === 'string') { + } else if (typeof pipeline === 'string' && !rawFieldNames) { const field = pipeline.substring(1); if (schema.fields[field] && schema.fields[field].type === 'Pointer') { return `$_p_${field}`; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 9343c78b81..7218b1a48f 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -2273,7 +2273,8 @@ export class PostgresStorageAdapter implements StorageAdapter { hint: ?mixed, explain?: boolean, _comment?: ?string, - _rawValues?: boolean + _rawValues?: boolean, + _rawFieldNames?: boolean ) { debug('aggregate'); const values = [className]; diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index 2a74f9b86d..19c945265b 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -126,7 +126,8 @@ export interface StorageAdapter { hint: ?mixed, explain?: boolean, comment?: string, - rawValues?: boolean + rawValues?: boolean, + rawFieldNames?: boolean ): Promise; performInitialization(options: ?any): Promise; watch(callback: () => void): void; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 5989ad7b03..6d13bf6553 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1270,6 +1270,7 @@ class DatabaseController { explain, comment, rawValues, + rawFieldNames, }: any = {}, auth: any = {}, validSchemaController: SchemaController.SchemaController @@ -1411,7 +1412,8 @@ class DatabaseController { hint, explain, comment, - rawValues + rawValues, + rawFieldNames ); } } else if (explain) { diff --git a/src/RestQuery.js b/src/RestQuery.js index 064b6fb353..f94c0af2c5 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -220,6 +220,7 @@ function _UnsafeRestQuery( case 'readPreference': case 'comment': case 'rawValues': + case 'rawFieldNames': this.findOptions[option] = restOptions[option]; break; case 'order': diff --git a/src/Routers/AggregateRouter.js b/src/Routers/AggregateRouter.js index ce569fd709..aab5b35494 100644 --- a/src/Routers/AggregateRouter.js +++ b/src/Routers/AggregateRouter.js @@ -31,6 +31,10 @@ export class AggregateRouter extends ClassesRouter { options.rawValues = body.rawValues; delete body.rawValues; } + if (typeof body.rawFieldNames === 'boolean') { + options.rawFieldNames = body.rawFieldNames; + delete body.rawFieldNames; + } options.pipeline = AggregateRouter.getPipeline(body); if (typeof body.where === 'string') { try { @@ -49,7 +53,7 @@ export class AggregateRouter extends ClassesRouter { req.info.clientSDK, req.info.context ); - if (!options.rawValues) { + if (!options.rawValues && !options.rawFieldNames) { for (const result of response.results) { if (typeof result === 'object') { UsersRouter.removeHiddenProperties(result); From 34494addd7e52103417c937599e3906adb29d3c0 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:58:12 +0200 Subject: [PATCH 07/13] test: Add rawFieldNames coverage for name preservation on input and output --- spec/ParseQuery.Aggregate.spec.js | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 7e799fe3b9..597357aeb7 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -1675,4 +1675,43 @@ describe('Parse.Query Aggregate testing', () => { expect(results.length).toBe(1); expect(results[0].total).toBe(1); }); + + it_id('f01a0002-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('rawFieldNames: true does NOT rewrite Parse-style names', async () => { + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + // Using Parse-style `createdAt` under rawFieldNames should query a field that doesn't exist in MongoDB. + const pipeline = [ + { $match: { _id: obj.id, createdAt: { $lte: { $date: iso } } } }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { + rawValues: true, + rawFieldNames: true, + useMasterKey: true, + }); + // `createdAt` is not a MongoDB field name; no documents match. + expect(results.length).toBe(0); + }); + + it_id('f01a0002-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('rawFieldNames: true returns native field names in results', async () => { + const obj = new TestObject(); + await obj.save(); + const pipeline = [ + { $match: { _id: obj.id } }, + { $project: { _id: 1, _created_at: 1 } }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { + rawValues: true, + rawFieldNames: true, + useMasterKey: true, + }); + expect(results.length).toBe(1); + expect(results[0]._id).toBe(obj.id); + expect(Object.prototype.hasOwnProperty.call(results[0], '_created_at')).toBe(true); + expect(Object.prototype.hasOwnProperty.call(results[0], 'objectId')).toBe(false); + expect(Object.prototype.hasOwnProperty.call(results[0], 'createdAt')).toBe(false); + }); }); From 2f39a13b9f5aefb1bab457167f357b265ee9fbfe Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:30:19 +0200 Subject: [PATCH 08/13] feat: Add `query` server options namespace with aggregation raw defaults --- resources/buildConfigDefinitions.js | 2 ++ src/Options/Definitions.js | 21 +++++++++++++++++++++ src/Options/docs.js | 7 +++++++ src/Options/index.js | 15 +++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 8bfeb3799c..d9267e58d2 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -23,6 +23,7 @@ const nestedOptionTypes = [ 'PagesOptions', 'PagesRoute', 'PasswordPolicyOptions', + 'QueryServerOptions', 'RequestComplexityOptions', 'SecurityOptions', 'SchemaOptions', @@ -48,6 +49,7 @@ const nestedOptionEnvPrefix = { PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', ParseServerOptions: 'PARSE_SERVER_', PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_', + QueryServerOptions: 'PARSE_SERVER_QUERY_', RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_', RequestComplexityOptions: 'PARSE_SERVER_REQUEST_COMPLEXITY_', SchemaOptions: 'PARSE_SERVER_SCHEMA_', diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 8ebff9fa9f..77e5011354 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -512,6 +512,13 @@ module.exports.ParseServerOptions = { help: 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', action: parsers.objectParser, }, + query: { + env: 'PARSE_SERVER_QUERY', + help: 'Query-related server defaults.', + action: parsers.objectParser, + type: 'QueryServerOptions', + default: {}, + }, rateLimit: { env: 'PARSE_SERVER_RATE_LIMIT', help: "Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

\u2139\uFE0F Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and use case.", @@ -778,6 +785,20 @@ module.exports.SecurityOptions = { default: false, }, }; +module.exports.QueryServerOptions = { + aggregationRawFieldNames: { + env: 'PARSE_SERVER_QUERY_AGGREGATION_RAW_FIELD_NAMES', + help: 'When `true`, all aggregation queries default to using native MongoDB field names (no automatic `createdAt` \u2192 `_created_at` rewriting). Individual queries can still override this via the `rawFieldNames` option. Default is `false`.', + action: parsers.booleanParser, + default: false, + }, + aggregationRawValues: { + env: 'PARSE_SERVER_QUERY_AGGREGATION_RAW_VALUES', + help: 'When `true`, all aggregation queries default to using MongoDB Extended JSON (EJSON) for explicit value typing and skip schema-based value coercion. Individual queries can still override this via the `rawValues` option. Default is `false`.', + action: parsers.booleanParser, + default: false, + }, +}; module.exports.PagesOptions = { customRoutes: { env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES', diff --git a/src/Options/docs.js b/src/Options/docs.js index 7035c06862..09aff594d2 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -95,6 +95,7 @@ * @property {Boolean} protectedFieldsTriggerExempt Whether Cloud Code triggers (e.g. `beforeSave`, `afterSave`) are exempt from `protectedFields`. If `true`, triggers receive the full object including protected fields in `request.object` and `request.original`, regardless of the caller's auth context. If `false`, protected fields are stripped from the original object fetch used to build trigger objects. Defaults to `false`. * @property {Union} publicServerURL Optional. The public URL to Parse Server. This URL will be used to reach Parse Server publicly for features like password reset and email verification links. The option can be set to a string or a function that can be asynchronously resolved. The returned URL string must start with `http://` or `https://`. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications + * @property {QueryServerOptions} query Query-related server defaults. * @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and use case. * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes * @property {String[]} readOnlyMasterKeyIps (Optional) Restricts the use of read-only master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the read-only master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the read-only master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `"0.0.0.0/0,::0"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['0.0.0.0/0', '::0']` which means that any IP address is allowed to use the read-only master key. It is recommended to set this option to `['127.0.0.1', '::1']` to restrict access to `localhost`. @@ -154,6 +155,12 @@ * @property {Boolean} enableCheckLog Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs. */ +/** + * @interface QueryServerOptions + * @property {Boolean} aggregationRawFieldNames When `true`, all aggregation queries default to using native MongoDB field names (no automatic `createdAt` → `_created_at` rewriting). Individual queries can still override this via the `rawFieldNames` option. Default is `false`. + * @property {Boolean} aggregationRawValues When `true`, all aggregation queries default to using MongoDB Extended JSON (EJSON) for explicit value typing and skip schema-based value coercion. Individual queries can still override this via the `rawValues` option. Default is `false`. + */ + /** * @interface PagesOptions * @property {PagesRoute[]} customRoutes The custom routes. diff --git a/src/Options/index.js b/src/Options/index.js index 73009f1f10..68d1d61a94 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -391,6 +391,10 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_REQUEST_COMPLEXITY :DEFAULT: {} */ requestComplexity: ?RequestComplexityOptions; + /* Query-related server defaults. + :ENV: PARSE_SERVER_QUERY + :DEFAULT: {} */ + query: ?QueryServerOptions; /* The security options to identify and report weak security settings. :DEFAULT: {} */ security: ?SecurityOptions; @@ -490,6 +494,17 @@ export interface SecurityOptions { checkGroups: ?(CheckGroup[]); } +export interface QueryServerOptions { + /* When `true`, all aggregation queries default to using MongoDB Extended JSON (EJSON) for explicit value typing and skip schema-based value coercion. Individual queries can still override this via the `rawValues` option. Default is `false`. + :ENV: PARSE_SERVER_QUERY_AGGREGATION_RAW_VALUES + :DEFAULT: false */ + aggregationRawValues: ?boolean; + /* When `true`, all aggregation queries default to using native MongoDB field names (no automatic `createdAt` → `_created_at` rewriting). Individual queries can still override this via the `rawFieldNames` option. Default is `false`. + :ENV: PARSE_SERVER_QUERY_AGGREGATION_RAW_FIELD_NAMES + :DEFAULT: false */ + aggregationRawFieldNames: ?boolean; +} + export interface PagesOptions { /* Is true if pages should be localized; this has no effect on custom page redirects. :DEFAULT: false */ From fa0c705c0adec22b6475f031391b54d14b9d5773 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:36:20 +0200 Subject: [PATCH 09/13] feat: Resolve server-level `query.aggregationRaw*` defaults with per-query override --- spec/ParseQuery.Aggregate.spec.js | 59 +++++++++++++++++++++++++++++++ src/Routers/AggregateRouter.js | 10 ++++++ 2 files changed, 69 insertions(+) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 597357aeb7..3d81239acb 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -1714,4 +1714,63 @@ describe('Parse.Query Aggregate testing', () => { expect(Object.prototype.hasOwnProperty.call(results[0], 'objectId')).toBe(false); expect(Object.prototype.hasOwnProperty.call(results[0], 'createdAt')).toBe(false); }); + + it_id('f01a0003-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('server-level rawValues default applies when per-query omits it', async () => { + await reconfigureServer({ query: { aggregationRawValues: true } }); + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + const pipeline = [ + { $match: { objectId: obj.id, createdAt: { $lte: { $date: iso } } } }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + // No rawValues in the per-query options — should inherit from the server default. + const results = await query.aggregate(pipeline, { useMasterKey: true }); + expect(results.length).toBe(1); + expect(results[0].total).toBe(1); + }); + + it_id('f01a0003-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('per-query rawValues: false overrides server-level true', async () => { + await reconfigureServer({ query: { aggregationRawValues: true } }); + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + // With server-level rawValues: true, EJSON `{ $date: iso }` would be converted to a BSON Date + // and the $match would succeed. Per-query rawValues: false overrides that, so `{ $date: iso }` + // is NOT deserialized as EJSON and the comparison fails — proving the override works. + const pipeline = [ + { $match: { objectId: obj.id, createdAt: { $lte: { $date: iso } } } }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { + rawValues: false, + useMasterKey: true, + }); + // Under rawValues: false the `{ $date: iso }` is not EJSON-deserialized; comparison yields no match. + expect(results.length).toBe(0); + }); + + it_id('f01a0003-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('server-level rawFieldNames default applies when per-query omits it', async () => { + await reconfigureServer({ + query: { aggregationRawValues: true, aggregationRawFieldNames: true }, + }); + const obj = new TestObject(); + await obj.save(); + const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); + const pipeline = [ + { + $match: { + _id: obj.id, + _created_at: { $lte: { $date: iso } }, + }, + }, + { $count: 'total' }, + ]; + const query = new Parse.Query('TestObject'); + const results = await query.aggregate(pipeline, { useMasterKey: true }); + expect(results.length).toBe(1); + expect(results[0].total).toBe(1); + }); }); diff --git a/src/Routers/AggregateRouter.js b/src/Routers/AggregateRouter.js index aab5b35494..753be4e7e0 100644 --- a/src/Routers/AggregateRouter.js +++ b/src/Routers/AggregateRouter.js @@ -35,6 +35,16 @@ export class AggregateRouter extends ClassesRouter { options.rawFieldNames = body.rawFieldNames; delete body.rawFieldNames; } + const queryOptions = (req.config && req.config.query) || {}; + if (options.rawValues === undefined && typeof queryOptions.aggregationRawValues === 'boolean') { + options.rawValues = queryOptions.aggregationRawValues; + } + if ( + options.rawFieldNames === undefined && + typeof queryOptions.aggregationRawFieldNames === 'boolean' + ) { + options.rawFieldNames = queryOptions.aggregationRawFieldNames; + } options.pipeline = AggregateRouter.getPipeline(body); if (typeof body.where === 'string') { try { From 37c461cc85cd25b1d1e11eac64e47accf4af5c0b Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:39:19 +0200 Subject: [PATCH 10/13] fix: Remove unused parameters from Postgres aggregate signature to satisfy lint --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 7218b1a48f..08f8c647f4 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -2271,10 +2271,7 @@ export class PostgresStorageAdapter implements StorageAdapter { pipeline: any, readPreference: ?string, hint: ?mixed, - explain?: boolean, - _comment?: ?string, - _rawValues?: boolean, - _rawFieldNames?: boolean + explain?: boolean ) { debug('aggregate'); const values = [className]; From 7e4610daed7b78cf6ae7318f97df1159cc19d0a1 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:58:25 +0200 Subject: [PATCH 11/13] real UUIDs --- spec/ParseQuery.Aggregate.spec.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 3d81239acb..ac615b1fde 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -473,7 +473,7 @@ describe('Parse.Query Aggregate testing', () => { expect(new Date(results[0].date.iso)).toEqual(obj1.get('date')); }); - it_id('f01a0001-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('rawValues: true converts $date EJSON marker to BSON Date in $match', async () => { + it_id('8c211edc-a48e-4ab3-810a-f56897228393')(it_exclude_dbs(['postgres']))('rawValues: true converts $date EJSON marker to BSON Date in $match', async () => { const obj = new TestObject(); await obj.save(); const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); @@ -487,7 +487,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results[0].total).toBe(1); }); - it_id('f01a0001-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('rawValues: true deserializes $date at any nesting depth', async () => { + it_id('2a79e4c8-aa16-434f-bbea-e34637eaff16')(it_exclude_dbs(['postgres']))('rawValues: true deserializes $date at any nesting depth', async () => { const obj = new TestObject(); await obj.save(); const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); @@ -508,7 +508,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results[0].total).toBe(1); }); - it_id('f01a0001-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('rawValues: true does NOT coerce bare ISO strings', async () => { + it_id('cc08f092-8f26-4f5b-81f2-769de812982f')(it_exclude_dbs(['postgres']))('rawValues: true does NOT coerce bare ISO strings', async () => { const obj = new TestObject(); await obj.save(); const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); @@ -522,7 +522,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results.length).toBe(0); }); - it_id('f01a0001-0004-0004-0004-000000000004')(it_exclude_dbs(['postgres']))('rawValues: true does NOT coerce Parse Date encoding `{ __type: "Date", iso }`', async () => { + it_id('bc4cb19e-3114-40d8-8db8-0e9f5b582f33')(it_exclude_dbs(['postgres']))('rawValues: true does NOT coerce Parse Date encoding `{ __type: "Date", iso }`', async () => { const obj = new TestObject(); await obj.save(); const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); @@ -541,7 +541,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results.length).toBe(0); }); - it_id('f01a0001-0005-0005-0005-000000000005')(it_exclude_dbs(['postgres']))('rawValues: true serializes BSON Date in results as `{ $date: iso }`', async () => { + it_id('27c3bf01-5b4a-41b3-988e-522fdef63181')(it_exclude_dbs(['postgres']))('rawValues: true serializes BSON Date in results as `{ $date: iso }`', async () => { const obj = new TestObject(); await obj.save(); const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); @@ -556,7 +556,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results[0]._created_at).toEqual(jasmine.objectContaining({ $date: jasmine.any(String) })); }); - it_id('f01a0001-0006-0006-0006-000000000006')(it_exclude_dbs(['postgres']))('rawValues: true deserializes EJSON in `$addFields`', async () => { + it_id('5b6b225d-219e-480c-9241-ac3e146dda9f')(it_exclude_dbs(['postgres']))('rawValues: true deserializes EJSON in `$addFields`', async () => { const obj = new TestObject(); await obj.save(); const iso = '2026-01-01T00:00:00.000Z'; @@ -1653,7 +1653,7 @@ describe('Parse.Query Aggregate testing', () => { } }); - it_id('f01a0002-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('rawFieldNames: true lets users write _created_at directly', async () => { + it_id('e1d699e3-1389-4213-b0e6-37838bcef390')(it_exclude_dbs(['postgres']))('rawFieldNames: true lets users write _created_at directly', async () => { const obj = new TestObject(); await obj.save(); const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); @@ -1676,7 +1676,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results[0].total).toBe(1); }); - it_id('f01a0002-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('rawFieldNames: true does NOT rewrite Parse-style names', async () => { + it_id('79e68a9f-ce15-44cf-9f9e-6a722f73ef1a')(it_exclude_dbs(['postgres']))('rawFieldNames: true does NOT rewrite Parse-style names', async () => { const obj = new TestObject(); await obj.save(); const iso = new Date(obj.createdAt.getTime() + 1).toISOString(); @@ -1695,7 +1695,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results.length).toBe(0); }); - it_id('f01a0002-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('rawFieldNames: true returns native field names in results', async () => { + it_id('b69c1a5a-b1d3-4c45-adb4-bb8f74af37c6')(it_exclude_dbs(['postgres']))('rawFieldNames: true returns native field names in results', async () => { const obj = new TestObject(); await obj.save(); const pipeline = [ @@ -1715,7 +1715,7 @@ describe('Parse.Query Aggregate testing', () => { expect(Object.prototype.hasOwnProperty.call(results[0], 'createdAt')).toBe(false); }); - it_id('f01a0003-0001-0001-0001-000000000001')(it_exclude_dbs(['postgres']))('server-level rawValues default applies when per-query omits it', async () => { + it_id('f854cc3d-2259-42bc-be88-4122f80f8568')(it_exclude_dbs(['postgres']))('server-level rawValues default applies when per-query omits it', async () => { await reconfigureServer({ query: { aggregationRawValues: true } }); const obj = new TestObject(); await obj.save(); @@ -1731,7 +1731,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results[0].total).toBe(1); }); - it_id('f01a0003-0002-0002-0002-000000000002')(it_exclude_dbs(['postgres']))('per-query rawValues: false overrides server-level true', async () => { + it_id('5be28dc9-a298-488c-8dec-893c2309f6b7')(it_exclude_dbs(['postgres']))('per-query rawValues: false overrides server-level true', async () => { await reconfigureServer({ query: { aggregationRawValues: true } }); const obj = new TestObject(); await obj.save(); @@ -1752,7 +1752,7 @@ describe('Parse.Query Aggregate testing', () => { expect(results.length).toBe(0); }); - it_id('f01a0003-0003-0003-0003-000000000003')(it_exclude_dbs(['postgres']))('server-level rawFieldNames default applies when per-query omits it', async () => { + it_id('e0e89b62-5ced-4610-ab16-82ea532e69c1')(it_exclude_dbs(['postgres']))('server-level rawFieldNames default applies when per-query omits it', async () => { await reconfigureServer({ query: { aggregationRawValues: true, aggregationRawFieldNames: true }, }); From b987d49930e0db326321de15dc7d8fae6cd572c7 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:45:31 +0100 Subject: [PATCH 12/13] parse 8.6.0 --- package-lock.json | 186 +++++++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 126 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39fce0cb67..3d5affca6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "mongodb": "7.1.0", "mustache": "4.2.0", "otpauth": "9.5.0", - "parse": "8.5.0", + "parse": "8.6.0", "path-to-regexp": "8.4.0", "pg-monitor": "3.1.0", "pg-promise": "12.6.0", @@ -2128,18 +2128,18 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", - "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", + "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==", "license": "MIT", "dependencies": { "core-js-pure": "^3.48.0" @@ -4204,6 +4204,65 @@ "node": "20 || 22 || 24" } }, + "node_modules/@parse/push-adapter/node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@parse/push-adapter/node_modules/@babel/runtime-corejs3": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", + "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.48.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@parse/push-adapter/node_modules/parse": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-8.5.0.tgz", + "integrity": "sha512-X9gI4Yjbi9LPMPnCtKL4h0Nxe1aSCFMPWcB1zbu11qU/Be3eVSB5I5IMBunTuWlVz6Wchu3dtM5jl/1aBZ9wiQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "7.28.6", + "@babel/runtime-corejs3": "7.29.0", + "crypto-js": "4.2.0", + "idb-keyval": "6.2.2", + "react-native-crypto-js": "1.0.0", + "ws": "8.19.0" + }, + "engines": { + "node": ">=20.19.0 <21 || >=22.12.0 <23 || >=24.1.0 <25" + } + }, + "node_modules/@parse/push-adapter/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -10697,9 +10756,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz", - "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -20432,20 +20491,20 @@ } }, "node_modules/parse": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-8.5.0.tgz", - "integrity": "sha512-X9gI4Yjbi9LPMPnCtKL4h0Nxe1aSCFMPWcB1zbu11qU/Be3eVSB5I5IMBunTuWlVz6Wchu3dtM5jl/1aBZ9wiQ==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-8.6.0.tgz", + "integrity": "sha512-AZjc8yGo8/iTZFpCXWw/r1qNusiUGWtq9i92/u0jNd+Iupg3EJUSV/OOyTrCeav8NDyo92wVS5O3iKAYPlhlsA==", "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "7.28.6", - "@babel/runtime-corejs3": "7.29.0", + "@babel/runtime": "7.29.2", + "@babel/runtime-corejs3": "7.29.2", "crypto-js": "4.2.0", "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", - "ws": "8.19.0" + "ws": "8.20.0" }, "engines": { - "node": ">=20.19.0 <21 || >=22.12.0 <23 || >=24.1.0 <25" + "node": ">=20.19.0 <21 || >=22.13.0 <23 || >=24.1.0 <25" } }, "node_modules/parse-json": { @@ -20475,27 +20534,6 @@ "node": ">=6" } }, - "node_modules/parse/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -28385,14 +28423,14 @@ } }, "@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==" + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==" }, "@babel/runtime-corejs3": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", - "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", + "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==", "requires": { "core-js-pure": "^3.48.0" } @@ -29893,6 +29931,40 @@ "npmlog": "7.0.1", "parse": "8.5.0", "web-push": "3.6.7" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==" + }, + "@babel/runtime-corejs3": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", + "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "requires": { + "core-js-pure": "^3.48.0" + } + }, + "parse": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-8.5.0.tgz", + "integrity": "sha512-X9gI4Yjbi9LPMPnCtKL4h0Nxe1aSCFMPWcB1zbu11qU/Be3eVSB5I5IMBunTuWlVz6Wchu3dtM5jl/1aBZ9wiQ==", + "requires": { + "@babel/runtime": "7.28.6", + "@babel/runtime-corejs3": "7.29.0", + "crypto-js": "4.2.0", + "idb-keyval": "6.2.2", + "react-native-crypto-js": "1.0.0", + "ws": "8.19.0" + } + }, + "ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "requires": {} + } } }, "@pkgjs/parseargs": { @@ -34300,9 +34372,9 @@ } }, "core-js-pure": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz", - "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==" + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==" }, "core-util-is": { "version": "1.0.3", @@ -41049,24 +41121,16 @@ } }, "parse": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-8.5.0.tgz", - "integrity": "sha512-X9gI4Yjbi9LPMPnCtKL4h0Nxe1aSCFMPWcB1zbu11qU/Be3eVSB5I5IMBunTuWlVz6Wchu3dtM5jl/1aBZ9wiQ==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-8.6.0.tgz", + "integrity": "sha512-AZjc8yGo8/iTZFpCXWw/r1qNusiUGWtq9i92/u0jNd+Iupg3EJUSV/OOyTrCeav8NDyo92wVS5O3iKAYPlhlsA==", "requires": { - "@babel/runtime": "7.28.6", - "@babel/runtime-corejs3": "7.29.0", + "@babel/runtime": "7.29.2", + "@babel/runtime-corejs3": "7.29.2", "crypto-js": "4.2.0", "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", - "ws": "8.19.0" - }, - "dependencies": { - "ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "requires": {} - } + "ws": "8.20.0" } }, "parse-json": { diff --git a/package.json b/package.json index f3dfea3e4f..52d1c86df7 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "mongodb": "7.1.0", "mustache": "4.2.0", "otpauth": "9.5.0", - "parse": "8.5.0", + "parse": "8.6.0", "path-to-regexp": "8.4.0", "pg-monitor": "3.1.0", "pg-promise": "12.6.0", From 1957045938da954fb1c8d808fc36668c40c1bada Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:53:20 +0100 Subject: [PATCH 13/13] bump node floor --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c5764ee1a..9951d4f3ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,7 +103,7 @@ "yaml": "2.8.3" }, "engines": { - "node": ">=20.19.0 <21.0.0 || >=22.12.0 <23.0.0 || >=24.11.0 <25.0.0" + "node": ">=20.19.0 <21.0.0 || >=22.13.0 <23.0.0 || >=24.11.0 <25.0.0" }, "funding": { "type": "opencollective", diff --git a/package.json b/package.json index 487bce8869..3acd8279b8 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ }, "types": "types/index.d.ts", "engines": { - "node": ">=20.19.0 <21.0.0 || >=22.12.0 <23.0.0 || >=24.11.0 <25.0.0" + "node": ">=20.19.0 <21.0.0 || >=22.13.0 <23.0.0 || >=24.11.0 <25.0.0" }, "bin": { "parse-server": "bin/parse-server"