Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 38 additions & 1 deletion integration/test/ParseQueryTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ describe('Parse Query', () => {
await object.save();

const query = new Parse.Query(TestObject);
const results = await query.find({ json: true });
let results = await query.find({ json: true });
assert.strictEqual(results[0] instanceof Parse.Object, false);
assert.strictEqual(results[0].foo, 'bar');
assert.strictEqual(results[0].className, 'TestObject');
assert.strictEqual(results[0].objectId, object.id);

results = await query.findAll({ json: true });
assert.strictEqual(results[0] instanceof Parse.Object, false);
assert.strictEqual(results[0].foo, 'bar');
assert.strictEqual(results[0].className, 'TestObject');
Expand Down Expand Up @@ -101,6 +107,25 @@ describe('Parse Query', () => {
}
});

it('can do findAll query with count', async () => {
const items = [];
for (let i = 0; i < 4; i++) {
items.push(new TestObject({ countMe: true }));
}
await Parse.Object.saveAll(items);

const query = new Parse.Query(TestObject);
query.withCount(true);
const { results, count } = await query.findAll();

assert.equal(results.length, 4);
assert(typeof count === 'number');
assert.equal(count, 4);
for (let i = 0; i < 4; i++) {
assert.equal(results[i].className, 'TestObject');
}
});

it('can do query withCount set to false', async () => {
const items = [];
for (let i = 0; i < 4; i++) {
Expand Down Expand Up @@ -2391,6 +2416,18 @@ describe('Parse Query', () => {
assert.equal(indexName, '_id_');
});

it('can query with explain false', async () => {
const obj1 = new TestObject({ number: 1 });
const obj2 = new TestObject({ number: 2 });
const obj3 = new TestObject({ number: 3 });
await Parse.Object.saveAll([obj1, obj2, obj3]);

const query = new Parse.Query(TestObject);
query.explain(false);
const results = await query.find();
expect(results.length).toBe(3);
});

it('can query with select on null field', async () => {
const obj1 = new TestObject({ number: 1, arrayField: [] });
const obj2 = new TestObject({ number: 2, arrayField: [{ subfield: 1 }] });
Expand Down
2 changes: 1 addition & 1 deletion src/EventuallyQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ const EventuallyQueue = {
const { sessionToken } = queueObject.serverOptions;
const query = new ParseQuery(ObjectType);
query.equalTo('hash', queueObject.hash);
const results = await query.find({ sessionToken });
const results: any = await query.find({ sessionToken });
if (results.length > 0) {
return EventuallyQueue.sendQueueCallback(results[0], queueObject);
}
Expand Down
19 changes: 13 additions & 6 deletions src/ParseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ class ParseQuery<T extends ParseObject = ParseObject> {
* @returns {Promise} A promise that is resolved with the results when
* the query completes.
*/
find(options?: QueryOptions): Promise<T[]> {
find(options?: QueryOptions): Promise<T[] | { results: T[]; count: number }> {
Copy link

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Union return type is a breaking change for TypeScript consumers.

Changing find() from Promise<T[]> to Promise<T[] | { results: T[]; count: number }> forces every caller to narrow the type before accessing array methods. This was already flagged in the PR discussion by @mtrezza as conceptually problematic and inconsistent across SDKs. Consider a separate method (e.g., findWithCount()) or always returning a wrapper to avoid a discriminated union.

🤖 Prompt for AI Agents
In `@src/ParseQuery.ts` at line 678, The change to ParseQuery.find from
Promise<T[]> to Promise<T[] | { results: T[]; count: number }> introduces a
breaking discriminated union; revert the find signature back to find(options?:
QueryOptions): Promise<T[]> and implement a new method (e.g.,
findWithCount(options?: QueryOptions): Promise<{ results: T[]; count: number }>)
that returns the results + count wrapper, updating the ParseQuery class methods
accordingly (adjust implementations currently in find to be reused by
findWithCount) and update any callers/tests/types to use the new findWithCount
when they need the count.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could actually introduce a method findWithCount() for this, to not mess up the types while providing some relieve for developers - what do you think @dplewis?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

const findOptions = ParseObject._getRequestOptions(options);
this._setRequestTask(findOptions);

Expand Down Expand Up @@ -736,12 +736,15 @@ class ParseQuery<T extends ParseObject = ParseObject> {
* @returns {Promise} A promise that is resolved with the results when
* the query completes.
*/
async findAll(options?: BatchOptions): Promise<T[]> {
let result: T[] = [];
async findAll(options?: BatchOptions): Promise<T[] | { results: T[], count: number}> {
let results: T[] = [];
await this.eachBatch((objects: T[]) => {
result = [...result, ...objects];
results = [...results, ...objects];
}, options);
return result;
if (this._count) {
return { results, count: results.length };
}
return results;
}
Comment on lines +739 to 748
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

findAll with withCount returns a fabricated count instead of the server-provided total.

count: results.length (line 745) is the length of the locally accumulated array, not the server's authoritative count. While these should converge after all pages are fetched, it diverges from the semantics of find().withCount() where the count is server-computed (and can differ from results.length if, e.g., ACLs filter rows at query time vs. cursor time).

If withCount support is kept in findAll, consider capturing the server count from the first batch response instead.

🤖 Prompt for AI Agents
In `@src/ParseQuery.ts` around lines 739 - 748, The current findAll in ParseQuery
returns a fabricated count (results.length) instead of the server-provided
total; update findAll to capture and return the server count supplied by the
first batch response rather than computing results.length. Specifically, change
the eachBatch usage so the batch callback (from eachBatch) returns or exposes
server metadata (e.g., totalCount/ count from the server) and in findAll capture
that value on the first invocation (store in a local variable like serverCount)
and, when this._count is true, return { results, count: serverCount } instead of
count: results.length; adjust eachBatch (and its callback signature) to provide
the server count if it currently only yields objects.


/**
Expand Down Expand Up @@ -930,10 +933,14 @@ class ParseQuery<T extends ParseObject = ParseObject> {
return !finished;
},
async () => {
const [results] = await Promise.all([
const [response] = await Promise.all([
query.find(findOptions),
Promise.resolve(previousResults.length > 0 && callback(previousResults)),
]);
let results: any = response;
if (results.results) {
results = results.results;
}
Comment on lines +936 to +943
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unnecessary count=1 sent on every batch request & fragile unwrapping.

Two concerns here:

  1. Performance: eachBatch clones the query via fromJSON(this.className, this.toJSON()) (line 924), which preserves _count. Every paginated batch request sends count=1 to the server, forcing it to compute the total count on each page — completely wasted work. Strip _count from the cloned query used for pagination.

  2. Fragile duck-typing: results.results (line 941) relies on the assumption that a plain T[] never has a .results property. This works today but is brittle. A more explicit check (e.g., !Array.isArray(response)) would be safer.

Proposed fix
     const query = ParseQuery.fromJSON(this.className, this.toJSON());
+    query._count = false;
     query.ascending('objectId');
     query._limit = options.batchSize || 100;

And for the unwrapping:

-        let results: any = response;
-        if (results.results) {
-          results = results.results;
-        }
+        const results: T[] = Array.isArray(response)
+          ? response
+          : (response as { results: T[] }).results;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [response] = await Promise.all([
query.find(findOptions),
Promise.resolve(previousResults.length > 0 && callback(previousResults)),
]);
let results: any = response;
if (results.results) {
results = results.results;
}
const [response] = await Promise.all([
query.find(findOptions),
Promise.resolve(previousResults.length > 0 && callback(previousResults)),
]);
const results: T[] = Array.isArray(response)
? response
: (response as { results: T[] }).results;
🤖 Prompt for AI Agents
In `@src/ParseQuery.ts` around lines 936 - 943, The cloned query used by eachBatch
(created via fromJSON(this.className, this.toJSON())) is preserving the internal
_count flag and causing every query.find call to send count=1; strip or delete
the `_count` property from the cloned query before paginating so paged requests
don't force a total-count compute. Also make the response unwrapping in the
Promise.all result more robust by detecting whether the response is an array
(e.g., use Array.isArray(response)) instead of relying on the presence of
`results.results`; update the logic around `query.find`, `response`, and
`results` to unwrap only when the response is not an Array.

if (results.length >= query._limit) {
if (findOptions.json) {
query.greaterThan('objectId', (results[results.length - 1] as any).objectId);
Expand Down
10 changes: 8 additions & 2 deletions types/ParseQuery.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,10 @@ declare class ParseQuery<T extends ParseObject = ParseObject> {
* @returns {Promise} A promise that is resolved with the results when
* the query completes.
*/
find(options?: QueryOptions): Promise<T[]>;
find(options?: QueryOptions): Promise<T[] | {
results: T[];
count: number;
}>;
/**
* Retrieves a complete list of ParseObjects that satisfy this query.
* Using `eachBatch` under the hood to fetch all the valid objects.
Expand All @@ -241,7 +244,10 @@ declare class ParseQuery<T extends ParseObject = ParseObject> {
* @returns {Promise} A promise that is resolved with the results when
* the query completes.
*/
findAll(options?: BatchOptions): Promise<T[]>;
findAll(options?: BatchOptions): Promise<T[] | {
results: T[];
count: number;
}>;
/**
* Counts the number of objects that match this query.
*
Expand Down
10 changes: 6 additions & 4 deletions types/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,6 @@ async function test_cancel_query() {
query.cancel();
}

/* eslint-disable @typescript-eslint/no-redundant-type-constituents -- object is intentionally included for testing Exclude<FieldType, object>. */
type FieldType =
| string
| number
Expand All @@ -917,7 +916,7 @@ type FieldType =
| Parse.Pointer
| Parse.Polygon
| Parse.Relation;
/* eslint-enable @typescript-eslint/no-redundant-type-constituents */

async function test_schema(
anyField: FieldType,
notString: Exclude<FieldType, string>,
Expand Down Expand Up @@ -2046,9 +2045,12 @@ function testQuery() {
// $ExpectType ParseObject<Attributes>
await queryUntyped.get('objectId');

// $ExpectType ParseObject<Attributes>[]
// $ExpectType ParseObject<Attributes>[] | { results: ParseObject<Attributes>[]; count: number; }
await queryUntyped.find();

// $ExpectType ParseObject<Attributes>[] | { results: ParseObject<Attributes>[]; count: number; }
await queryUntyped.findAll();

// $ExpectType string[]
await queryTyped.distinct('example');

Expand All @@ -2058,7 +2060,7 @@ function testQuery() {
// $ExpectType ParseObject<{ example: string; }>
await queryTyped.get('objectId');

// $ExpectType ParseObject<{ example: string; }>[]
// $ExpectType ParseObject<{ example: string; }>[] | { results: ParseObject<{ example: string; }>[]; count: number; }
await queryTyped.find();

// $ExpectType ParseObject<{ example: string; }> | undefined
Expand Down
Loading