Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPRECATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS22 | Config option `protectedFieldsTriggerExempt` defaults to `true` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS23 | Config option `protectedFieldsSaveResponseExempt` defaults to `false` | | 9.7.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS24 | Config option `installation.duplicateDeviceTokenActionEnforceAuth` defaults to `true` | [#10451](https://github.com/parse-community/parse-server/pull/10451) | 9.9.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS25 | Database option `databaseOptions.explainResultsAsArray` defaults to `true` | [#7442](https://github.com/parse-community/parse-server/issues/7442) | 9.11.0 (2026) | 10.0.0 (2027) | deprecated | - |

[i_deprecation]: ## "The version and date of the deprecation."
[i_change]: ## "The version and date of the planned change."
Expand Down
45 changes: 45 additions & 0 deletions spec/ParseQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5564,6 +5564,51 @@ describe('Parse.Query testing', () => {
expect(resultWithMasterKey).toBeDefined();
}
);

it_only_db('mongo')(
'explain results are returned as an array when databaseOptions.explainResultsAsArray is true (#7442)',
async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI: 'mongodb://localhost:27017/parse',
databaseOptions: {
explainResultsAsArray: true,
},
});

const obj = new TestObject({ foo: 'bar' });
await obj.save();

const query = new Parse.Query(TestObject);
query.explain();
const result = await query.find({ useMasterKey: true });
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(1);
expect(result[0].executionStats).not.toBeUndefined();
}
);

it_only_db('mongo')(
'explain results are a single object by default, consistent with legacy behaviour (#7442)',
async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI: 'mongodb://localhost:27017/parse',
databaseOptions: {
explainResultsAsArray: undefined,
},
});

const obj = new TestObject({ foo: 'bar' });
await obj.save();

const query = new Parse.Query(TestObject);
query.explain();
const result = await query.find({ useMasterKey: true });
expect(Array.isArray(result)).toBe(false);
expect(result.executionStats).not.toBeUndefined();
}
);
});

describe('query input type validation', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,11 @@ export class Config {
} else if (typeof databaseOptions.allowPublicExplain !== 'boolean') {
throw `Parse Server option 'databaseOptions.allowPublicExplain' must be a boolean.`;
}
if (databaseOptions.explainResultsAsArray === undefined) {
databaseOptions.explainResultsAsArray = DatabaseOptions.explainResultsAsArray.default;
} else if (typeof databaseOptions.explainResultsAsArray !== 'boolean') {
throw `Parse Server option 'databaseOptions.explainResultsAsArray' must be a boolean.`;
}
}

static validateLiveQueryOptions(liveQuery) {
Expand Down
11 changes: 10 additions & 1 deletion src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -1558,7 +1558,16 @@ class DatabaseController {
);
}
} else if (explain) {
return this.adapter.find(className, schema, query, queryOptions);
return this.adapter
.find(className, schema, query, queryOptions)
.then(results =>
// Normalise MongoDB's single explain object to an array, consistent with `find`
// and the Postgres adapter (which already returns an array). Opt-in until the
// default flips in a future major, see databaseOptions.explainResultsAsArray.
this.options.databaseOptions?.explainResultsAsArray && !Array.isArray(results)
? [results]
: results
);
} else {
return this.adapter
.find(className, schema, query, queryOptions)
Expand Down
5 changes: 5 additions & 0 deletions src/Deprecator/Deprecations.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,9 @@ module.exports = [
changeNewDefault: 'false',
solution: "Set 'allowAggregationForReadOnlyMasterKey' to 'false' to prevent the read-only master key from running aggregation pipelines, which can include write-capable stages (e.g. '$out', '$merge'). Set to 'true' to keep the current behavior where the read-only master key can run aggregation pipelines.",
},
{
optionKey: 'databaseOptions.explainResultsAsArray',
changeNewDefault: 'true',
solution: "Set 'databaseOptions.explainResultsAsArray' to 'true' to return MongoDB 'explain' results as an array, consistent with 'find' and the Postgres adapter. Set to 'false' to keep the current behavior where MongoDB returns a single object.",
},
];
6 changes: 6 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,12 @@ module.exports.DatabaseOptions = {
action: parsers.booleanParser,
default: false,
},
explainResultsAsArray: {
env: 'PARSE_SERVER_DATABASE_EXPLAIN_RESULTS_AS_ARRAY',
help: 'Set to `true` to return `Parse.Query.explain` results for MongoDB as an array, consistent with `find` and the Postgres adapter. When `false`, MongoDB returns a single explain object (legacy behaviour), which is inconsistent with `find` and breaks strongly-typed SDKs that expect an array.',
action: parsers.booleanParser,
default: false,
},
forceServerObjectId: {
env: 'PARSE_SERVER_DATABASE_FORCE_SERVER_OBJECT_ID',
help: 'The MongoDB driver option to force server to assign _id values instead of driver.',
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,9 @@ export interface DatabaseOptions {
/* Set to `true` to allow `Parse.Query.explain` without master key.<br><br>⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.
:DEFAULT: false */
allowPublicExplain: ?boolean;
/* Set to `true` to return `Parse.Query.explain` results for MongoDB as an array, consistent with `find` and the Postgres adapter. When `false`, MongoDB returns a single explain object (legacy behaviour), which is inconsistent with `find` and breaks strongly-typed SDKs that expect an array.
:DEFAULT: false */
explainResultsAsArray: ?boolean;
/* An array of MongoDB client event configurations to enable logging of specific events. */
logClientEvents: ?(LogClientEvent[]);
/* Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead. */
Expand Down
1 change: 1 addition & 0 deletions src/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const ParseServerDatabaseOptions = [
'createIndexUserUsernameCaseInsensitive',
'disableIndexFieldValidation',
'enableSchemaHooks',
'explainResultsAsArray',
'logClientEvents',
'maxTimeMS',
'schemaCacheTtl',
Expand Down
Loading