Skip to content

Commit 17ceef1

Browse files
authored
feat: Add rawValues and rawFieldNames options to aggregate queries (#3021)
1 parent 20bc3a9 commit 17ceef1

3 files changed

Lines changed: 193 additions & 4 deletions

File tree

src/ParseQuery.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ export type GetOptions = QueryOptions;
3131

3232
export type FirstOptions = QueryOptions;
3333

34+
export interface AggregateOptions extends QueryOptions {
35+
rawValues?: boolean;
36+
rawFieldNames?: boolean;
37+
}
38+
3439
export interface FullTextOptions {
3540
language?: string;
3641
caseSensitive?: boolean;
@@ -796,9 +801,27 @@ class ParseQuery<T extends ParseObject = ParseObject> {
796801
* Executes an aggregate query and returns aggregate results
797802
*
798803
* @param {(Array|object)} pipeline Array or Object of stages to process query
804+
* @param {object} options Valid options are:<ul>
805+
* <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
806+
* be used for this request. Defaults to `true` when not provided, for
807+
* backward compatibility.
808+
* <li>sessionToken: A valid session token, used for making a request on
809+
* behalf of a specific user.
810+
* <li>context: A dictionary that is accessible in Cloud Code triggers.
811+
* <li>rawValues: When `true`, disables schema-based value transformation
812+
* in the pipeline. Pipeline values are interpreted using MongoDB
813+
* Extended JSON (EJSON), so typed values such as `{ $date: '...' }`,
814+
* `{ $oid: '...' }`, `{ $numberDecimal: '...' }`, etc. are converted
815+
* to their corresponding BSON types by the server. Requires Parse
816+
* Server 9.9.0+
817+
* <li>rawFieldNames: When `true`, disables automatic field-name
818+
* transformation (e.g. `createdAt` → `_created_at`) in the pipeline.
819+
* Users write native MongoDB field names directly. Requires Parse
820+
* Server 9.9.0+
821+
* </ul>
799822
* @returns {Promise} A promise that is resolved with the query completes.
800823
*/
801-
aggregate(pipeline: any): Promise<any[]> {
824+
aggregate(pipeline: any, options?: AggregateOptions): Promise<any[]> {
802825
if (!Array.isArray(pipeline) && typeof pipeline !== 'object') {
803826
throw new Error('Invalid pipeline must be Array or Object');
804827
}
@@ -808,13 +831,22 @@ class ParseQuery<T extends ParseObject = ParseObject> {
808831
}
809832
pipeline.unshift({ $match: this._where });
810833
}
811-
const params = {
834+
const params: Record<string, any> = {
812835
pipeline,
813836
hint: this._hint,
814837
explain: this._explain,
815838
readPreference: this._readPreference,
816839
};
817-
const aggregateOptions = { useMasterKey: true };
840+
if (options?.rawValues !== undefined) {
841+
params.rawValues = options.rawValues;
842+
}
843+
if (options?.rawFieldNames !== undefined) {
844+
params.rawFieldNames = options.rawFieldNames;
845+
}
846+
const aggregateOptions = ParseObject._getRequestOptions(options);
847+
if (aggregateOptions.useMasterKey === undefined) {
848+
aggregateOptions.useMasterKey = true;
849+
}
818850
this._setRequestTask(aggregateOptions);
819851

820852
const controller = CoreManager.getQueryController();

src/__tests__/ParseQuery-test.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2851,6 +2851,141 @@ describe('ParseQuery', () => {
28512851
});
28522852
});
28532853

2854+
it('can pass rawValues option to aggregate query', async () => {
2855+
const pipeline = [{ group: { objectId: '$name' } }];
2856+
let capturedParams;
2857+
let capturedOptions;
2858+
CoreManager.setQueryController({
2859+
find() {},
2860+
aggregate(className, params, options) {
2861+
expect(className).toBe('Item');
2862+
capturedParams = params;
2863+
capturedOptions = options;
2864+
return Promise.resolve({ results: [] });
2865+
},
2866+
});
2867+
2868+
const q = new ParseQuery('Item');
2869+
await q.aggregate(pipeline, { rawValues: true });
2870+
2871+
expect(capturedParams.rawValues).toBe(true);
2872+
expect('rawValues' in capturedOptions).toBe(false);
2873+
expect(capturedOptions.useMasterKey).toBe(true);
2874+
});
2875+
2876+
it('can pass rawFieldNames option to aggregate query', async () => {
2877+
const pipeline = [{ group: { objectId: '$name' } }];
2878+
let capturedParams;
2879+
let capturedOptions;
2880+
CoreManager.setQueryController({
2881+
find() {},
2882+
aggregate(className, params, options) {
2883+
expect(className).toBe('Item');
2884+
capturedParams = params;
2885+
capturedOptions = options;
2886+
return Promise.resolve({ results: [] });
2887+
},
2888+
});
2889+
2890+
const q = new ParseQuery('Item');
2891+
await q.aggregate(pipeline, { rawFieldNames: true });
2892+
2893+
expect(capturedParams.rawFieldNames).toBe(true);
2894+
expect('rawFieldNames' in capturedOptions).toBe(false);
2895+
expect(capturedOptions.useMasterKey).toBe(true);
2896+
});
2897+
2898+
it('aggregate defaults useMasterKey to true when no options provided', async () => {
2899+
const pipeline = [{ group: { objectId: '$name' } }];
2900+
let capturedOptions;
2901+
CoreManager.setQueryController({
2902+
find() {},
2903+
aggregate(_className, _params, options) {
2904+
capturedOptions = options;
2905+
return Promise.resolve({ results: [] });
2906+
},
2907+
});
2908+
2909+
const q = new ParseQuery('Item');
2910+
await q.aggregate(pipeline);
2911+
2912+
expect(capturedOptions.useMasterKey).toBe(true);
2913+
});
2914+
2915+
it('aggregate allows useMasterKey to be overridden via options', async () => {
2916+
const pipeline = [{ group: { objectId: '$name' } }];
2917+
let capturedOptions;
2918+
CoreManager.setQueryController({
2919+
find() {},
2920+
aggregate(_className, _params, options) {
2921+
capturedOptions = options;
2922+
return Promise.resolve({ results: [] });
2923+
},
2924+
});
2925+
2926+
const q = new ParseQuery('Item');
2927+
await q.aggregate(pipeline, { useMasterKey: false });
2928+
2929+
expect(capturedOptions.useMasterKey).toBe(false);
2930+
});
2931+
2932+
it('aggregate does not leak raw* keys into params when unset', async () => {
2933+
const pipeline = [{ group: { objectId: '$name' } }];
2934+
let capturedParams;
2935+
CoreManager.setQueryController({
2936+
find() {},
2937+
aggregate(_className, params, _options) {
2938+
capturedParams = params;
2939+
return Promise.resolve({ results: [] });
2940+
},
2941+
});
2942+
2943+
const q = new ParseQuery('Item');
2944+
await q.aggregate(pipeline, { useMasterKey: false });
2945+
2946+
expect('rawValues' in capturedParams).toBe(false);
2947+
expect('rawFieldNames' in capturedParams).toBe(false);
2948+
});
2949+
2950+
it('aggregate forwards sessionToken via request options', async () => {
2951+
const pipeline = [{ group: { objectId: '$name' } }];
2952+
let capturedOptions;
2953+
CoreManager.setQueryController({
2954+
find() {},
2955+
aggregate(_className, _params, options) {
2956+
capturedOptions = options;
2957+
return Promise.resolve({ results: [] });
2958+
},
2959+
});
2960+
2961+
const q = new ParseQuery('Item');
2962+
await q.aggregate(pipeline, { sessionToken: 'r:abc', useMasterKey: false });
2963+
2964+
expect(capturedOptions.sessionToken).toBe('r:abc');
2965+
expect(capturedOptions.useMasterKey).toBe(false);
2966+
});
2967+
2968+
it('aggregate forwards raw* options when explicitly false', async () => {
2969+
const pipeline = [{ group: { objectId: '$name' } }];
2970+
let capturedParams;
2971+
CoreManager.setQueryController({
2972+
find() {},
2973+
aggregate(_className, params, _options) {
2974+
capturedParams = params;
2975+
return Promise.resolve({ results: [] });
2976+
},
2977+
});
2978+
2979+
const q = new ParseQuery('Item');
2980+
await q.aggregate(pipeline, {
2981+
rawValues: false,
2982+
rawFieldNames: false,
2983+
});
2984+
2985+
expect(capturedParams.rawValues).toBe(false);
2986+
expect(capturedParams.rawFieldNames).toBe(false);
2987+
});
2988+
28542989
it('can cancel query', async () => {
28552990
const mockRequestTask = {
28562991
abort: () => {},

types/ParseQuery.d.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export type FindOptions = QueryOptions;
1616
export type CountOptions = BaseRequestOptions;
1717
export type GetOptions = QueryOptions;
1818
export type FirstOptions = QueryOptions;
19+
export interface AggregateOptions extends QueryOptions {
20+
rawValues?: boolean;
21+
rawFieldNames?: boolean;
22+
}
1923
export interface FullTextOptions {
2024
language?: string;
2125
caseSensitive?: boolean;
@@ -262,9 +266,27 @@ declare class ParseQuery<T extends ParseObject = ParseObject> {
262266
* Executes an aggregate query and returns aggregate results
263267
*
264268
* @param {(Array|object)} pipeline Array or Object of stages to process query
269+
* @param {object} options Valid options are:<ul>
270+
* <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
271+
* be used for this request. Defaults to `true` when not provided, for
272+
* backward compatibility.
273+
* <li>sessionToken: A valid session token, used for making a request on
274+
* behalf of a specific user.
275+
* <li>context: A dictionary that is accessible in Cloud Code triggers.
276+
* <li>rawValues: When `true`, disables schema-based value transformation
277+
* in the pipeline. Pipeline values are interpreted using MongoDB
278+
* Extended JSON (EJSON), so typed values such as `{ $date: '...' }`,
279+
* `{ $oid: '...' }`, `{ $numberDecimal: '...' }`, etc. are converted
280+
* to their corresponding BSON types by the server. Requires Parse
281+
* Server 9.9.0+
282+
* <li>rawFieldNames: When `true`, disables automatic field-name
283+
* transformation (e.g. `createdAt` → `_created_at`) in the pipeline.
284+
* Users write native MongoDB field names directly. Requires Parse
285+
* Server 9.9.0+
286+
* </ul>
265287
* @returns {Promise} A promise that is resolved with the query completes.
266288
*/
267-
aggregate(pipeline: any): Promise<any[]>;
289+
aggregate(pipeline: any, options?: AggregateOptions): Promise<any[]>;
268290
/**
269291
* Retrieves at most one Parse.Object that satisfies this query.
270292
*

0 commit comments

Comments
 (0)