Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat: Add GraphQL bulk mutations createMany, updateMany, deleteMany - #10341

#10341
Open
Moumouls wants to merge 12 commits into
parse-community:alphaparse-community/parse-server:alphafrom
Moumouls:moumouls/create-update-delete-many-gqlMoumouls/parse-server:moumouls/create-update-delete-many-gqlCopy head branch name to clipboard
Open

feat: Add GraphQL bulk mutations createMany, updateMany, deleteMany#10341
Moumouls wants to merge 12 commits into
parse-community:alphaparse-community/parse-server:alphafrom
Moumouls:moumouls/create-update-delete-many-gqlMoumouls/parse-server:moumouls/create-update-delete-many-gqlCopy head branch name to clipboard

Conversation

@Moumouls

@Moumouls Moumouls commented Mar 28, 2026

Copy link
Copy Markdown
Member

Pull Request

Issue

Support batch OPS on GQL api

Approach

Introduce 3 new mutations, with promise all settled strategy and batch request limit security policy

made with Cursor Composer 2

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)
  • Add security check
  • Add new Parse Error codes to Parse JS SDK

Summary by CodeRabbit

  • New Features

    • Added bulk GraphQL mutations: createMany, updateMany, deleteMany, including optional alias configuration.
    • Bulk mutation responses include per-item results preserving input order and a standardized per-item error shape.
  • Behavior / Limits

    • Enforced batch/request-size limits for bulk operations, with master-key bypass and improved limit error messaging.
    • Supports mixed per-item outcomes (successes return updated/created data; failures return per-item error details).
  • Validation

    • Expanded validation for bulk mutation flags and alias types with clearer rejection on invalid inputs.
  • Tests

    • Added extensive coverage for limits, ordering, partial failures, and error sanitization.

@parse-github-assistant

Copy link
Copy Markdown

I will reformat the title to use the proper commit message syntax.

@parse-github-assistant parse-github-assistant Bot changed the title feat: create, update, delete many graphql feat: Create, update, delete many graphql Mar 28, 2026
@parse-github-assistant

parse-github-assistant Bot commented Mar 28, 2026

Copy link
Copy Markdown

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Tip

  • Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs.
  • Use Test Driven Development. Write failing tests before implementing functionality. Ensure tests pass.
  • Group code into logical blocks. Add a short comment before each block to explain its purpose.
  • We offer conceptual guidance. Coding is up to you. PRs must be merge-ready for human review.
  • Our review focuses on concept, not quality. PRs with code issues will be rejected. Use an AI agent.
  • Human review time is precious. Avoid review ping-pong. Inspect and test your AI-generated code.

Note

Please respond to review comments from AI agents just like you would to comments from a human reviewer. Let the reviewer resolve their own comments, unless they have reviewed and accepted your commit, or agreed with your explanation for why the feedback was incorrect.

Caution

Pull requests must be written using an AI agent with human supervision. Pull requests written entirely by a human will likely be rejected, because of lower code quality, higher review effort and the higher risk of introducing bugs. Please note that AI review comments on this pull request alone do not satisfy this requirement.

@parseplatformorg

parseplatformorg commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds configurable per-class bulk GraphQL mutations for create, update, and delete operations, with ordered per-item results, batch-limit validation, standardized error payloads, shared limit helpers, and expanded schema, controller, runtime, and error-normalization tests.

Changes

Bulk GraphQL mutations

Layer / File(s) Summary
Bulk mutation configuration and GraphQL types
src/Controllers/ParseGraphQLController.js, src/GraphQL/loaders/defaultGraphQLTypes.js, src/GraphQL/loaders/parseClassTypes.js, spec/ParseGraphQLController.spec.js
Mutation flags and aliases are validated and exposed; bulk error and updateMany input types are registered and tested.
Bulk mutation generation and execution
src/GraphQL/loaders/parseClassMutations.js, spec/ParseGraphQLSchema.spec.js, spec/ParseGraphQLServer.spec.js
Adds configurable createMany, updateMany, and deleteMany mutations with input validation, concurrent per-item execution, ordered results, alias support, and partial-failure coverage.
Bulk error normalization
src/Error.js, spec/Utils.spec.js
Normalizes Parse and native failures into sanitized or detailed code/message payloads, including safe handling of malformed error objects.
Shared batch-limit enforcement
src/batchRequestLimit.js, src/batch.js
Centralizes batch-limit lookup and authorization-aware enforcement for reuse by the REST batch handler.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  actor Client
  participant GraphQLServer
  participant ParseCore
  participant ErrorHandler
  Client->>GraphQLServer: Submit bulk mutation
  GraphQLServer->>ParseCore: Execute per-item operations
  ParseCore-->>GraphQLServer: Return fulfilled and rejected results
  GraphQLServer->>ErrorHandler: Normalize rejected reasons
  ErrorHandler-->>GraphQLServer: Return code and message payloads
  GraphQLServer-->>Client: Return ordered results
Loading

Suggested reviewers: mtrezza


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Engage In Review Feedback ❌ Error Several review comments remain unaddressed: unbounded fan-out remains, Parse.Error sanitization remains, and the spec still uses toBeRejected(). Implement the requested fixes or document/retract the objections in the review thread before resolving the comments.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Security Check ⚠️ Warning createMany/updateMany/deleteMany accept unbounded arrays when requestComplexity.batchRequestLimit is unset (-1 default), so Promise.allSettled can enable DoS/resource exhaustion. Add a hard cap or safer positive default for GraphQL bulk inputs, or throttle/constrain concurrency before processing the full list.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the PR's main change and satisfies the required feat: prefix and capitalization rule.
Description check ✅ Passed The description follows the required template with Issue, Approach, and Tasks sections and is mostly complete.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Moumouls

Copy link
Copy Markdown
Member Author

@coderabbitai does all security measures are correctly implemented to prevent abuse, do you see security gaps ?

@Moumouls

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me examine the code changes in this PR to give you a thorough security assessment.


🏁 Script executed:

#!/bin/bash
# Look at the changed files in this PR related to GraphQL batch mutations
fd -e js -e ts . --search-path src/GraphQL | xargs grep -l -i "many\|batch\|createMany\|updateMany\|deleteMany" 2>/dev/null | head -20

echo "---"
# Search for batch limit config
rg -n "batchLimit\|batch_limit\|maxBatch\|max_batch\|BatchSize\|batchSize" --type js --type ts -C 3

echo "---"
# Search for the new mutations
rg -n "createMany\|updateMany\|deleteMany" --type js --type ts -C 5 | head -200

echo "---"
# Look at security policy for batch
rg -n "settled\|allSettled\|Promise.all" --type js --type ts -C 3 | grep -i "batch\|many\|graphql" -A 3 -B 3 | head -100

Length of output: 173


🏁 Script executed:

#!/bin/bash
echo "=== parseClassMutations.js ==="
cat src/GraphQL/loaders/parseClassMutations.js

echo ""
echo "=== mutation.js transformer ==="
cat src/GraphQL/transformers/mutation.js

echo ""
echo "=== defaultGraphQLTypes.js (relevant parts) ==="
grep -n "many\|batch\|limit\|Many\|Batch\|Limit" src/GraphQL/loaders/defaultGraphQLTypes.js -A 5 -B 2 | head -150

echo ""
echo "=== parseClassTypes.js (relevant parts) ==="
grep -n "many\|batch\|limit\|Many\|Batch\|Limit" src/GraphQL/loaders/parseClassTypes.js -A 5 -B 2 | head -100

echo ""
echo "=== ParseServer options for batch/graphql ==="
rg -n "graphQLBatch\|batchLimit\|maxBatch\|graphqlBatch\|graphql.*batch\|batch.*graphql\|maxObjects\|objectLimit" --type js --type ts -C 3 | head -100

echo ""
echo "=== ParseServerOptions or Definitions ==="
fd -e js -e ts "Definitions\|Options\|Config" --search-path src | xargs grep -l "graphql\|GraphQL" 2>/dev/null | head -10

echo ""
echo "=== ACL and CLP checks in mutations ==="
grep -n "ACL\|CLP\|classLevelPermissions\|requiresAuthentication\|auth\|masterKey" src/GraphQL/loaders/parseClassMutations.js | head -50

echo ""
echo "=== Rate limiting ==="
rg -n "rateLimit\|rate_limit\|throttle" --type js --type ts -C 3 | head -60

Length of output: 41577


🏁 Script executed:

#!/bin/bash
echo "=== batchRequestLimit file ==="
fd "batchRequestLimit" | head -5
fd "batchRequestLimit" | xargs cat 2>/dev/null

echo ""
echo "=== ParseServerOptions - looking for batch-related config ==="
fd "Options" --search-path src | head -10
fd "Definitions" --search-path src | head -5

echo ""
echo "=== OptionsDefinitions or ParseServerOptions ==="
fd -e js -e ts "OptionsDefinitions" | xargs cat 2>/dev/null | grep -A 5 -B 2 -i "batch\|bulk\|graphql" | head -100

echo ""
echo "=== default value for batch limit ==="
rg -n "DEFAULT\|default.*[Ll]imit\|[Ll]imit.*default\|batchRequestLimit\|100\|25\|50" src/batchRequestLimit* 2>/dev/null | head -40

echo ""
echo "=== masterKey bypass for batch limit ==="
rg -n "masterKey\|isMaster\|useMasterKey" src/batchRequestLimit* 2>/dev/null | head -20

echo ""
echo "=== error info leakage - bulkErrorFromReason ==="
grep -n "bulkErrorFromReason\|INTERNAL_SERVER_ERROR\|message\|reason" src/GraphQL/loaders/parseClassMutations.js | head -20

echo ""
echo "=== duplicate IDs check in deleteMany ==="
grep -n "duplicate\|Set\|unique\|dedup" src/GraphQL/loaders/parseClassMutations.js | head -20

echo ""
echo "=== tests for batch mutations ==="
fd -e js -e ts -e spec . | xargs grep -l "createMany\|updateMany\|deleteMany\|batchRequestLimit" 2>/dev/null | head -10

echo ""
echo "=== Test file content for batch security ==="
fd -e js -e spec "batch\|graphql.*many\|many.*graphql\|batchRequest" | head -5

Length of output: 2235


🏁 Script executed:

#!/bin/bash
echo "=== OptionsDefinitions batch/requestComplexity section ==="
grep -n "batchRequestLimit\|requestComplexity\|batch" src/Options/Definitions.js | head -40

echo ""
echo "=== RequestComplexity spec ==="
cat spec/RequestComplexity.spec.js | grep -A 20 -B 5 "batch\|graphql\|many\|createMany\|updateMany\|deleteMany" | head -150

echo ""
echo "=== ParseGraphQLServer.spec - batch mutation tests ==="
grep -n "createMany\|updateMany\|deleteMany\|batchRequestLimit\|allowCreateMany\|allowUpdateMany\|allowDeleteMany" spec/ParseGraphQLServer.spec.js | head -60

echo ""
echo "=== Check if batchRequestLimit is in OptionsDefinitions ==="
grep -n "batchRequestLimit\|requestComplexity" src/Options/Definitions.js | head -20

echo ""
echo "=== Check for concurrent connection / memory exhaustion protection ==="
rg -n "concurrency\|semaphore\|throttle\|queue\|chunk\|MAX_CONCURRENT\|CONCURRENCY" --type js --type ts -C 3 | head -60

echo ""
echo "=== Check how errors are surfaced from non-Parse errors in bulkErrorFromReason ==="
grep -n -A 10 "bulkErrorFromReason" src/GraphQL/loaders/parseClassMutations.js | head -30

echo ""
echo "=== Check if there's a max input size / GraphQL depth limiting ==="
rg -n "depthLimit\|depth_limit\|maxDepth\|complexityLimit\|queryDepth\|queryComplexity" --type js --type ts -C 3 | head -40

echo ""
echo "=== Check how allowDeleteMany is defaulted - verify no unintended exposure ==="
grep -n "allowDeleteMany\|allowUpdateMany\|allowCreateMany\|isDeleteManyEnabled\|isUpdateManyEnabled\|isCreateManyEnabled" src/GraphQL/loaders/parseClassMutations.js | head -30

Length of output: 7362


@Moumouls, here is a thorough security assessment of the batch mutations introduced in this PR.


✅ Security Measures That Are Correctly Implemented

Measure Detail
Batch size enforcement assertBulkInputLength checks count > 0 and calls isBatchRequestLimitExceeded before any work starts
Master/Maintenance key bypass Consistent with existing REST batch behavior in src/batch.js
Auth & CLP propagation auth, config, info are passed to every objectsMutations.createObject/updateObject/deleteObject call — CLP/ACL enforcement is handled by the existing mutation layer
Partial failure isolation Promise.allSettled ensures one failure doesn't abort others — correct approach for bulk ops
Mutation enable flags respected allowCreateMany/updateMany/deleteMany check existing isCreateEnabled / isUpdateEnabled / isDestroyEnabled flags
Error sanitization (outer) The outer catch (e) routes through parseGraphQLSchema.handleError(e) which sanitizes errors for the whole request

🔴 Security Gaps Found

1. Default Batch Limit is Unlimited (-1)

In src/batchRequestLimit.js:

function getBatchRequestLimit(config) {
  return config?.requestComplexity?.batchRequestLimit ?? -1;
}

The default is -1 — meaning no limit at all unless the operator explicitly configures one. A single createMany call with 100,000 items is accepted by default. This is a denial-of-service risk (memory, DB connections, CPU). Consider:

  • Defaulting to a safe value (e.g., 50 or 100) instead of -1
  • Or at minimum adding a prominent warning in the documentation

2. No Concurrency Cap — All Items Dispatched in Parallel

All three mutations use:

const settled = await Promise.allSettled(
  fieldsList.map(fieldSet => (async () => { ... })())
);

Even if batchRequestLimit is set to e.g. 100, all 100 operations are fired simultaneously. There is no chunking, semaphore, or concurrency limit. This can:

  • Saturate the database connection pool
  • Cause memory spikes proportional to batch size
  • Still produce a soft-DoS even within the configured limit

Consider processing items in chunks (e.g., 10 at a time) or using a concurrency limiter like p-limit.


3. Raw Error Message Leakage in bulkErrorFromReason

const bulkErrorFromReason = reason => {
  if (reason instanceof Parse.Error) {
    return { code: reason.code, message: reason.message };
  }
  const message =
    reason && typeof reason.message === 'string' ? reason.message : 'Internal server error';
  return { code: Parse.Error.INTERNAL_SERVER_ERROR, message };
};

For non-Parse.Error exceptions (e.g., database adapter errors, network errors, ORM internals), the raw .message string is returned directly to the client. This can expose:

  • Database schema or collection names
  • Internal connection details
  • Stack trace fragments in some adapter errors

The outer catch uses parseGraphQLSchema.handleError(e) which sanitizes — but errors caught inside Promise.allSettled bypass that sanitizer. The fix should either use a fixed message or re-wrap through the existing sanitizer:

// Safer approach
return { code: Parse.Error.INTERNAL_SERVER_ERROR, message: 'Internal server error' };

4. Duplicate IDs Not Deduplicated in deleteMany

In deleteMany, the ids array is processed as-is. Sending duplicate IDs (e.g., ids: ["abc", "abc"]) will:

  1. Attempt to delete the object once (succeeds)
  2. Attempt to delete it again (returns object-not-found error)

This is not a critical vulnerability since the second attempt fails gracefully, but it's unexpected behavior that should be documented or guarded against, since it can also be used to probe object existence.


5. deleteMany / updateMany Are Implicitly Enabled When destroy/update Is Enabled

const isDeleteManyEnabled = deleteManyExplicit !== undefined ? deleteManyExplicit : isDestroyEnabled;
const allowDeleteMany = isDeleteManyEnabled && (isDestroyEnabled || deleteManyExplicit === true);

Any operator who already exposed single delete/update mutations automatically opts into bulk delete/update without any action on their part. Given the amplified attack surface of bulk operations, this should arguably be an explicit opt-in (false by default), or the documentation should prominently highlight this behavior.


Summary Table

Gap Severity Notes
Default limit is -1 (unlimited) 🔴 High Easy DoS without explicit config
No concurrency cap on Promise.allSettled 🟠 Medium Resource exhaustion within configured limit
Non-Parse error message leakage 🟠 Medium Internal info exposed via per-item errors
No dedup of IDs in deleteMany 🟡 Low Unexpected behavior / existence probing
Implicit opt-in for bulk mutations 🟡 Low Surprising for existing deployments

🧠 Learnings used
Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-11-17T15:02:48.786Z
Learning: For Parse Server PRs, always suggest an Angular commit convention PR title that would make a meaningful changelog entry for developers. Update the PR title suggestion on every commit. The format should be: type(scope): description. Common types include feat, fix, perf, refactor, docs, test, chore. The scope should identify the subsystem (e.g., graphql, rest, push, security). The description should be action-oriented and clearly convey the change's impact to developers.

@codecov

codecov Bot commented Mar 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.89617% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.77%. Comparing base (5838c07) to head (0e8d9f0).
⚠️ Report is 8 commits behind head on alpha.

Files with missing lines Patch % Lines
src/GraphQL/loaders/parseClassMutations.js 93.61% 9 Missing ⚠️
src/Controllers/ParseGraphQLController.js 83.33% 2 Missing ⚠️
src/Error.js 88.88% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            alpha   #10341      +/-   ##
==========================================
+ Coverage   93.55%   93.77%   +0.21%     
==========================================
  Files         192      193       +1     
  Lines       16832    17006     +174     
  Branches      248      248              
==========================================
+ Hits        15747    15947     +200     
+ Misses       1064     1038      -26     
  Partials       21       21              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
spec/ParseGraphQLServer.spec.js (3)

12162-12222: Strengthen the partial-failure create assertion to verify rejected items are not persisted.

This test currently proves the successful item exists, but it doesn’t prove the rejected "FAIL" item was not created.

♻️ Suggested assertion hardening
               const q = new Parse.Query('BulkTest');
               q.equalTo('title', 'ok');
               const saved = await q.find({ useMasterKey: true });
               expect(saved.length).toBe(1);
+
+              const blocked = new Parse.Query('BulkTest');
+              blocked.equalTo('title', 'FAIL');
+              expect(await blocked.count({ useMasterKey: true })).toBe(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/ParseGraphQLServer.spec.js` around lines 12162 - 12222, Add an assertion
that the rejected item with title 'FAIL' was not persisted: after the existing
check that saved 'ok' exists, create a new Parse.Query('BulkTest') (e.g., qFail)
with qFail.equalTo('title', 'FAIL') and await qFail.find({ useMasterKey: true })
and assert its length is 0 (or that no objects are returned). This uses the
existing Parse.Query usage in the test and the createManyBulkTest flow to
confirm the rejected item wasn't saved.

11979-12015: Add a negative-control auth test for batch-limit bypass.

You validate master-key bypass, but a security regression test should also assert that non-master auth still enforces batchRequestLimit (for example JS key/session, and maintenance key if policy says master-only bypass).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/ParseGraphQLServer.spec.js` around lines 11979 - 12015, Add a
negative-control test alongside the existing "should bypass batchRequestLimit
for master key on createMany" that verifies non-master auth does NOT bypass the
limit: use reconfigureGraphQLWithBatchLimit2AndOpenClient() to set limit=2, then
call the same createMany mutation via apolloClient but send non-master
credentials (e.g. replace 'X-Parse-Master-Key' with a JS key/session header or
omit it, and optionally test maintenance key if policy applies) and assert the
request is rejected or truncated (expect an error or results.length <= 2) to
confirm batchRequestLimit is enforced for non-master auth; reference the
existing mutation name CreateManyBulkTest/createManyBulkTest, clientMutationId
usage, and the request context headers to locate where to add the new test.

11793-11835: Consider consolidating duplicated BulkTest bootstrap logic.

Schema reset/recreate is repeated in both beforeEach and reconfigureGraphQLWithBatchLimit2AndOpenClient; centralizing this into one helper would reduce drift and test maintenance cost.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/ParseGraphQLServer.spec.js` around lines 11793 - 11835, Duplicate
Bootstrap: Extract the repeated Parse.Schema setup (purge/delete/addString/save
and parseGraphQLServer.parseGraphQLSchema.schemaCache.clear()) into a single
helper (e.g., resetBulkTestSchema or bootstrapBulkTestSchema) and call it from
both beforeEach and reconfigureGraphQLWithBatchLimit2AndOpenClient; if updateCLP
should only run after server reconfigure, keep updateCLP('BulkTest', ...) inside
reconfigureGraphQLWithBatchLimit2AndOpenClient, otherwise add an optional
parameter to the helper so the CLP update can be performed there when requested.
src/GraphQL/loaders/parseClassMutations.js (3)

37-52: Inconsistent error creation pattern.

The empty-input case uses createSanitizedError (line 39) while the batch-limit exceeded case uses new Parse.Error directly (line 47). Both should likely use the same pattern for consistency. If createSanitizedError is needed for security reasons, it should also be applied to the batch limit error.

♻️ Suggested fix for consistency
   if (isBatchRequestLimitExceeded(count, config, auth)) {
     const batchRequestLimit = getBatchRequestLimit(config);
-    throw new Parse.Error(
+    throw createSanitizedError(
       Parse.Error.INVALID_JSON,
-      `bulk input contains ${count} items, which exceeds the limit of ${batchRequestLimit}.`
+      `bulk input contains ${count} items, which exceeds the limit of ${batchRequestLimit}.`,
+      config
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/GraphQL/loaders/parseClassMutations.js` around lines 37 - 52, The
function assertBulkInputLength inconsistently constructs errors: it uses
createSanitizedError for the zero-count case but constructs a raw Parse.Error
for the batch-limit case; update the batch-limit branch (inside
assertBulkInputLength, where isBatchRequestLimitExceeded and
getBatchRequestLimit are used) to throw a sanitized error via
createSanitizedError (matching the same error type/message pattern used for the
empty-input case) so both branches use the same secure error-creation helper
instead of new Parse.Error.

508-521: Optional: Consider extracting shared result mapping logic.

The result mapping logic (lines 508-521, 667-680, 781-794) is duplicated across all three bulk mutations. A shared helper could reduce duplication:

const mapSettledToResults = (settled, queryName) =>
  settled.map(r =>
    r.status === 'fulfilled'
      ? { success: true, [queryName]: r.value, error: null }
      : { success: false, [queryName]: null, error: bulkErrorFromReason(r.reason) }
  );

Also applies to: 667-680, 781-794

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/GraphQL/loaders/parseClassMutations.js` around lines 508 - 521, The
mapping of settled Promise results is duplicated across three bulk mutation
blocks; extract a single helper (e.g., mapSettledToResults(settled, queryName))
that takes the settled array and the getGraphQLQueryName string and returns the
unified result shape using bulkErrorFromReason for failures, then replace the
duplicated mapping in each bulk mutation with calls to this helper (update
usages where getGraphQLQueryName and settled are passed). Ensure the helper is
exported/available in the same module and that all three mutation handlers (the
blocks using settled and getGraphQLQueryName) call mapSettledToResults to remove
the duplicated logic.

435-506: Consider adding concurrency control for database operations.

Promise.allSettled executes all operations concurrently without throttling. Even within the batch limit, large batches could overwhelm the database with simultaneous writes. Consider using a concurrency-limited execution pattern (e.g., p-limit or chunked sequential execution) if batch limits can be set high.

Additionally, the inner cloneArgs at line 438 appears redundant since fieldsList is already cloned at line 428. This creates unnecessary object allocations per item.

♻️ Suggested fix to remove redundant cloning
           fieldsList.map(fieldSet =>
             (async () => {
-              let fields = fieldSet ? cloneArgs({ fields: fieldSet }).fields : {};
-              if (!fields) {
-                fields = {};
-              }
+              const fields = fieldSet || {};
               const parseFields = await transformTypes('create', fields, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/GraphQL/loaders/parseClassMutations.js` around lines 435 - 506, The batch
create loop uses Promise.allSettled over fieldsList.map which fires all DB
writes concurrently and also calls cloneArgs per item (cloneArgs({ fields:
fieldSet }).fields) redundantly; fix by removing the inner cloneArgs usage and
reuse the already-cloned fieldsList, and replace the raw
Promise.allSettled(fieldsList.map(...)) concurrency with a limited-execution
pattern (e.g., p-limit or manual chunking) around the async worker that runs
transformTypes and objectsMutations.createObject so only N creates run at once
while still collecting results (you can still call Promise.allSettled on the
limited set of promises), keeping the rest of the logic (transformTypes,
objectsMutations.createObject, objectsQueries.getObject, filterDeletedFields)
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/GraphQL/loaders/defaultGraphQLTypes.js`:
- Line 1282: The call addGraphQLType(PARSE_GRAPHQL_BULK_ERROR, true, true)
disables reserved-name and duplicate-type checks which can silently allow schema
collisions; change the call to not bypass these guards (e.g., remove the true,
true flags or pass false,false) so addGraphQLType(PARSE_GRAPHQL_BULK_ERROR) or
addGraphQLType(PARSE_GRAPHQL_BULK_ERROR, false, false) is used, and if there is
a legitimate reason to bypass, add explicit collision handling and a comment
next to the PARSE_GRAPHQL_BULK_ERROR registration instead of using
ignoreReserved.

In `@src/GraphQL/loaders/parseClassMutations.js`:
- Around line 594-598: The updateMany branch extracts updateEntry.fields without
cloning, risking mutation of the original args; update it to deep-clone the
fields using the same helper used elsewhere (cloneArgs) before passing them to
transformTypes/processing so each updateEntry uses const fields =
cloneArgs(updateEntry.fields || {}) (or equivalent), referencing the updateMany
async block and the updateEntry variable so behavior matches createMany and
update handlers.

---

Nitpick comments:
In `@spec/ParseGraphQLServer.spec.js`:
- Around line 12162-12222: Add an assertion that the rejected item with title
'FAIL' was not persisted: after the existing check that saved 'ok' exists,
create a new Parse.Query('BulkTest') (e.g., qFail) with qFail.equalTo('title',
'FAIL') and await qFail.find({ useMasterKey: true }) and assert its length is 0
(or that no objects are returned). This uses the existing Parse.Query usage in
the test and the createManyBulkTest flow to confirm the rejected item wasn't
saved.
- Around line 11979-12015: Add a negative-control test alongside the existing
"should bypass batchRequestLimit for master key on createMany" that verifies
non-master auth does NOT bypass the limit: use
reconfigureGraphQLWithBatchLimit2AndOpenClient() to set limit=2, then call the
same createMany mutation via apolloClient but send non-master credentials (e.g.
replace 'X-Parse-Master-Key' with a JS key/session header or omit it, and
optionally test maintenance key if policy applies) and assert the request is
rejected or truncated (expect an error or results.length <= 2) to confirm
batchRequestLimit is enforced for non-master auth; reference the existing
mutation name CreateManyBulkTest/createManyBulkTest, clientMutationId usage, and
the request context headers to locate where to add the new test.
- Around line 11793-11835: Duplicate Bootstrap: Extract the repeated
Parse.Schema setup (purge/delete/addString/save and
parseGraphQLServer.parseGraphQLSchema.schemaCache.clear()) into a single helper
(e.g., resetBulkTestSchema or bootstrapBulkTestSchema) and call it from both
beforeEach and reconfigureGraphQLWithBatchLimit2AndOpenClient; if updateCLP
should only run after server reconfigure, keep updateCLP('BulkTest', ...) inside
reconfigureGraphQLWithBatchLimit2AndOpenClient, otherwise add an optional
parameter to the helper so the CLP update can be performed there when requested.

In `@src/GraphQL/loaders/parseClassMutations.js`:
- Around line 37-52: The function assertBulkInputLength inconsistently
constructs errors: it uses createSanitizedError for the zero-count case but
constructs a raw Parse.Error for the batch-limit case; update the batch-limit
branch (inside assertBulkInputLength, where isBatchRequestLimitExceeded and
getBatchRequestLimit are used) to throw a sanitized error via
createSanitizedError (matching the same error type/message pattern used for the
empty-input case) so both branches use the same secure error-creation helper
instead of new Parse.Error.
- Around line 508-521: The mapping of settled Promise results is duplicated
across three bulk mutation blocks; extract a single helper (e.g.,
mapSettledToResults(settled, queryName)) that takes the settled array and the
getGraphQLQueryName string and returns the unified result shape using
bulkErrorFromReason for failures, then replace the duplicated mapping in each
bulk mutation with calls to this helper (update usages where getGraphQLQueryName
and settled are passed). Ensure the helper is exported/available in the same
module and that all three mutation handlers (the blocks using settled and
getGraphQLQueryName) call mapSettledToResults to remove the duplicated logic.
- Around line 435-506: The batch create loop uses Promise.allSettled over
fieldsList.map which fires all DB writes concurrently and also calls cloneArgs
per item (cloneArgs({ fields: fieldSet }).fields) redundantly; fix by removing
the inner cloneArgs usage and reuse the already-cloned fieldsList, and replace
the raw Promise.allSettled(fieldsList.map(...)) concurrency with a
limited-execution pattern (e.g., p-limit or manual chunking) around the async
worker that runs transformTypes and objectsMutations.createObject so only N
creates run at once while still collecting results (you can still call
Promise.allSettled on the limited set of promises), keeping the rest of the
logic (transformTypes, objectsMutations.createObject, objectsQueries.getObject,
filterDeletedFields) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 106ad4bd-4221-4cf9-a5fe-e8118dec6ca2

📥 Commits

Reviewing files that changed from the base of the PR and between 705855c and 7813f36.

📒 Files selected for processing (9)
  • spec/ParseGraphQLController.spec.js
  • spec/ParseGraphQLSchema.spec.js
  • spec/ParseGraphQLServer.spec.js
  • src/Controllers/ParseGraphQLController.js
  • src/GraphQL/loaders/defaultGraphQLTypes.js
  • src/GraphQL/loaders/parseClassMutations.js
  • src/GraphQL/loaders/parseClassTypes.js
  • src/batch.js
  • src/batchRequestLimit.js

Comment thread src/GraphQL/loaders/defaultGraphQLTypes.js Outdated
Comment thread src/GraphQL/loaders/parseClassMutations.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
spec/Utils.spec.js (1)

292-323: Add coverage for primitive/non-Error rejection reasons

bulkErrorPayloadFromReason accepts unknown, but tests only cover Parse.Error and native Error. Add at least one primitive/nullish case to prevent regressions in Promise rejection normalization.

Proposed test additions
   describe('bulkErrorPayloadFromReason', () => {
+    it('should handle primitive non-Parse reasons', () => {
+      const config = { enableSanitizedErrorResponse: false };
+      const payload = bulkErrorPayloadFromReason('plain failure', config);
+      expect(payload.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
+      expect(payload.message).toBe('plain failure');
+    });
+
+    it('should handle undefined reason', () => {
+      const config = { enableSanitizedErrorResponse: true };
+      const payload = bulkErrorPayloadFromReason(undefined, config);
+      expect(payload.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
+      expect(payload.message).toBe('Internal server error');
+    });
+
     it('should sanitize Parse.Error messages when enableSanitizedErrorResponse is true', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/Utils.spec.js` around lines 292 - 323, Add a new unit test case to cover
primitive and nullish rejection reasons for bulkErrorPayloadFromReason: create
tests that pass null, undefined and a primitive (e.g., a string or number) as
the reason and assert the returned payload.code is
Parse.Error.INTERNAL_SERVER_ERROR and payload.message is the sanitized message
('Internal server error') when config.enableSanitizedErrorResponse is true, and
the raw primitive/string value (or a stringified representation for non-strings)
when enableSanitizedErrorResponse is false; reference the existing describe
block and function bulkErrorPayloadFromReason to locate where to add these cases
so Promise rejection normalization for unknown reasons is covered.
spec/ParseGraphQLServer.spec.js (2)

11801-11873: Consolidate duplicated GraphQL reconfigure/client bootstrap helpers.

reconfigureGraphQLWithBatchLimit2AndOpenClient and reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient duplicate most setup/teardown logic. Extract one parameterized helper to reduce drift.

♻️ Proposed refactor
-async function reconfigureGraphQLWithBatchLimit2AndOpenClient() {
-  parseServer = await global.reconfigureServer({ requestComplexity: { batchRequestLimit: 2 } });
-  await createGQLFromParseServer(parseServer);
-  const httpLink = await createUploadLink({ uri: 'http://localhost:13377/graphql', fetch, headers });
-  apolloClient = new ApolloClient({ ... });
-  // schema + CLP setup
-}
-
-async function reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient() {
-  parseServer = await global.reconfigureServer({
-    maintenanceKey: 'test2',
-    maxUploadSize: '1kb',
-    enableSanitizedErrorResponse: false,
-  });
-  await createGQLFromParseServer(parseServer);
-  const httpLink = await createUploadLink({ uri: 'http://localhost:13377/graphql', fetch, headers });
-  apolloClient = new ApolloClient({ ... });
-  // schema + CLP setup
-}
+async function reconfigureBulkGraphQLAndOpenClient(serverConfig) {
+  parseServer = await global.reconfigureServer(serverConfig);
+  await createGQLFromParseServer(parseServer);
+  const httpLink = await createUploadLink({ uri: 'http://localhost:13377/graphql', fetch, headers });
+  apolloClient = new ApolloClient({ ... });
+  // schema + CLP setup (shared)
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/ParseGraphQLServer.spec.js` around lines 11801 - 11873, The two helpers
reconfigureGraphQLWithBatchLimit2AndOpenClient and
reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient share identical
bootstrapping (createGQLFromParseServer, createUploadLink + ApolloClient,
Parse.Schema purge/delete/addString,
parseGraphQLServer.parseGraphQLSchema.schemaCache.clear, and updateCLP); extract
a single parameterized helper (e.g., reconfigureGraphQLAndOpenClient(options))
that accepts the parseServer config object (requestComplexity or
enableSanitizedErrorResponse/etc.) and performs the shared steps, replace both
original functions with calls to this new helper passing the different config
objects, and update all callers to use it so no duplicate setup/teardown logic
remains.

12155-12195: Pin deleteMany duplicate-ID behavior with an explicit test.

deleteManyBulkTest tests success and partial failure, but not repeated IDs in the same request. Add one test so behavior is deterministic and protected against probing-style regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/ParseGraphQLServer.spec.js` around lines 12155 - 12195, Add a new test
case to pin deleteMany duplicate-ID behavior: create two objects (e.g., via
Parse.Object and Parse.Object.saveAll), then call the deleteManyBulkTest GraphQL
mutation (using apolloClient.mutate and toGlobalId) with an input.ids array that
contains the same object's global ID twice and assert the deterministic behavior
— e.g., that the results array length, per-item success flags, and which
objectIds are returned match the intended spec (either deduplicated or treated
as separate attempts). Place this alongside the existing "should deleteMany"
test so future changes to deleteManyBulkTest's handling of duplicate IDs are
caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@spec/ParseGraphQLServer.spec.js`:
- Around line 12265-12321: Add a regression test in ParseGraphQLServer.spec.js
that mirrors the existing createMany bulk test but has Parse.Cloud.beforeSave
throw a plain Error (e.g., new Error('internal details')) for one item; call
reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient() and run the
createManyBulkTest mutation via apolloClient.mutate, then assert the failing
item's results entry has success === false and that results[1].error.message
does NOT equal or contain the original thrown message (and does not leak
internal details) to ensure generic Error thrown from hooks is sanitized in bulk
responses.

In `@src/Error.js`:
- Around line 60-65: The code computing detailedMessage (in src/Error.js) can
throw when accessing reason.message or calling String(reason); wrap the
serialization in a safe path: implement a small safeSerialize helper or use a
try/catch around the existing conditional that attempts to read reason.message
and toString the reason, and fall back to a constant like 'Internal server
error' if any access or conversion throws or returns non-string; update the
detailedMessage assignment to call this safe serializer so bulk error mapping
cannot propagate exceptions.

In `@src/GraphQL/loaders/parseClassMutations.js`:
- Around line 586-604: The updateMany flow does not validate that updateEntry.id
is present before calling normalizeObjectIdForClass and
objectsMutations.updateObject; add an early validation in the
parseClassMutations updateMany loop (where updateEntry,
normalizeObjectIdForClass, transformTypes and objectsMutations.updateObject are
used) to throw a clear GraphQL/user error if id is missing or empty, e.g., check
if updateEntry.id is falsy and return/throw a descriptive error prior to
normalizing and parsing fields; apply the same validation to the single-update
handler earlier in this file (the update mutation handling the same
normalizeObjectIdForClass/updateObject sequence) to ensure consistent behavior.

---

Nitpick comments:
In `@spec/ParseGraphQLServer.spec.js`:
- Around line 11801-11873: The two helpers
reconfigureGraphQLWithBatchLimit2AndOpenClient and
reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient share identical
bootstrapping (createGQLFromParseServer, createUploadLink + ApolloClient,
Parse.Schema purge/delete/addString,
parseGraphQLServer.parseGraphQLSchema.schemaCache.clear, and updateCLP); extract
a single parameterized helper (e.g., reconfigureGraphQLAndOpenClient(options))
that accepts the parseServer config object (requestComplexity or
enableSanitizedErrorResponse/etc.) and performs the shared steps, replace both
original functions with calls to this new helper passing the different config
objects, and update all callers to use it so no duplicate setup/teardown logic
remains.
- Around line 12155-12195: Add a new test case to pin deleteMany duplicate-ID
behavior: create two objects (e.g., via Parse.Object and Parse.Object.saveAll),
then call the deleteManyBulkTest GraphQL mutation (using apolloClient.mutate and
toGlobalId) with an input.ids array that contains the same object's global ID
twice and assert the deterministic behavior — e.g., that the results array
length, per-item success flags, and which objectIds are returned match the
intended spec (either deduplicated or treated as separate attempts). Place this
alongside the existing "should deleteMany" test so future changes to
deleteManyBulkTest's handling of duplicate IDs are caught.

In `@spec/Utils.spec.js`:
- Around line 292-323: Add a new unit test case to cover primitive and nullish
rejection reasons for bulkErrorPayloadFromReason: create tests that pass null,
undefined and a primitive (e.g., a string or number) as the reason and assert
the returned payload.code is Parse.Error.INTERNAL_SERVER_ERROR and
payload.message is the sanitized message ('Internal server error') when
config.enableSanitizedErrorResponse is true, and the raw primitive/string value
(or a stringified representation for non-strings) when
enableSanitizedErrorResponse is false; reference the existing describe block and
function bulkErrorPayloadFromReason to locate where to add these cases so
Promise rejection normalization for unknown reasons is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f03fa34-7ccb-4880-9356-a512ca2ec8e1

📥 Commits

Reviewing files that changed from the base of the PR and between 7813f36 and 4196811.

📒 Files selected for processing (4)
  • spec/ParseGraphQLServer.spec.js
  • spec/Utils.spec.js
  • src/Error.js
  • src/GraphQL/loaders/parseClassMutations.js

Comment thread spec/ParseGraphQLServer.spec.js
Comment thread src/Error.js Outdated
Comment thread src/GraphQL/loaders/parseClassMutations.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
spec/Utils.spec.js (1)

292-349: Consider adding an explicit default-config test for sanitization fallback.

You cover explicit true/false; adding config omitted (undefined) would lock in the default secure behavior and prevent regressions.

➕ Optional test addition
   describe('bulkErrorPayloadFromReason', () => {
+    it('should sanitize non-Parse reasons when config is undefined', () => {
+      const payload = bulkErrorPayloadFromReason(new Error('internal stack trace'));
+      expect(payload.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
+      expect(payload.message).toBe('Internal server error');
+    });
+
     it('should sanitize Parse.Error messages when enableSanitizedErrorResponse is true', () => {
       const config = { enableSanitizedErrorResponse: true };
       const reason = new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Cloud script detail');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/Utils.spec.js` around lines 292 - 349, Add a test that verifies
bulkErrorPayloadFromReason defaults to the sanitized behavior when the config
argument is omitted: create a new spec that calls
bulkErrorPayloadFromReason(reason) with no config for both a Parse.Error (e.g.,
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Cloud script detail')) and a generic
Error (e.g., new Error('internal stack trace')), and assert that the returned
payloads use the sanitized messages ("Permission denied" for the Parse.Error
case and "Internal server error" for the generic error) while keeping the
appropriate codes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@spec/Utils.spec.js`:
- Around line 292-349: Add a test that verifies bulkErrorPayloadFromReason
defaults to the sanitized behavior when the config argument is omitted: create a
new spec that calls bulkErrorPayloadFromReason(reason) with no config for both a
Parse.Error (e.g., new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Cloud script
detail')) and a generic Error (e.g., new Error('internal stack trace')), and
assert that the returned payloads use the sanitized messages ("Permission
denied" for the Parse.Error case and "Internal server error" for the generic
error) while keeping the appropriate codes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e7144027-46d7-42b7-9293-84d6e32b144d

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc83d9 and 8f24cdb.

📒 Files selected for processing (2)
  • spec/Utils.spec.js
  • src/Error.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Error.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
spec/ParseGraphQLServer.spec.js (1)

12323-12428: ⚠️ Potential issue | 🟠 Major

These "plain Error" assertions still miss the generic bulk-error contract.

By asserting SCRIPT_FAILED on Line 12371 and Line 12423, these tests are only locking in the wrapped-hook path. They do not protect the GraphQL response shape for true non-Parse.Error per-item failures, which is the leakage-sensitive branch this PR was missing. Please add a case that actually reaches that branch, or update these expectations if hook-thrown Errors are supposed to surface as INTERNAL_SERVER_ERROR instead.

Based on learnings: For GraphQL bulk mutations in parse-community/parse-server, per-item failures must respect enableSanitizedErrorResponse at the GraphQL layer. When enableSanitizedErrorResponse is true (default), a plain Error thrown from hooks should be returned as { code: INTERNAL_SERVER_ERROR, message: 'Internal server error' }; when false, the original Error.message should be returned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/ParseGraphQLServer.spec.js` around lines 12323 - 12428, The assertions
for plain Error handling in createMany bulk tests are incorrect: instead of
expecting Parse.Error.SCRIPT_FAILED for hook-thrown plain Errors, update the
tests to assert the GraphQL-layer sanitized contract controlled by
enableSanitizedErrorResponse; when using
reconfigureGraphQLWithBatchLimit2AndOpenClient() (sanitization enabled) assert
the per-item failed.error.code equals Parse.Error.INTERNAL_SERVER_ERROR and
message equals 'Internal server error', and when using
reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient() (sanitization disabled)
assert failed.error.message equals the original 'internal stack detail'; keep
references to createManyBulkTest results and the beforeSave hook in the spec so
the test still triggers the same failure path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@spec/ParseGraphQLServer.spec.js`:
- Around line 12155-12194: Add a regression test for duplicate IDs to the
existing deleteMany spec: in the "should deleteMany" flow (the test invoking the
deleteManyBulkTest mutation) include a case where the same
toGlobalId('BulkTest', a.id) is passed twice in the input.ids array and assert
deterministic, contract-safe behavior (e.g., results.length === 2 and the second
entry reflects that the object was already deleted or otherwise indicates a
no-op/failure), and/or verify by querying Parse that the object is gone and no
inconsistent ordering/existence is exposed; update any related assertions in the
same test and mirror this duplicate-ID check in the other mentioned range (the
other test block around 12504-12574) so the resolver is guarded against
double-delete edge cases.

---

Duplicate comments:
In `@spec/ParseGraphQLServer.spec.js`:
- Around line 12323-12428: The assertions for plain Error handling in createMany
bulk tests are incorrect: instead of expecting Parse.Error.SCRIPT_FAILED for
hook-thrown plain Errors, update the tests to assert the GraphQL-layer sanitized
contract controlled by enableSanitizedErrorResponse; when using
reconfigureGraphQLWithBatchLimit2AndOpenClient() (sanitization enabled) assert
the per-item failed.error.code equals Parse.Error.INTERNAL_SERVER_ERROR and
message equals 'Internal server error', and when using
reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient() (sanitization disabled)
assert failed.error.message equals the original 'internal stack detail'; keep
references to createManyBulkTest results and the beforeSave hook in the spec so
the test still triggers the same failure path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2b926f70-2fe0-46a5-9689-02b8e00112cc

📥 Commits

Reviewing files that changed from the base of the PR and between 8f24cdb and 8b41f8d.

📒 Files selected for processing (1)
  • spec/ParseGraphQLServer.spec.js

Comment thread spec/ParseGraphQLServer.spec.js
@Moumouls

Copy link
Copy Markdown
Member Author

@coderabbitai why you do not approve ?

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@Moumouls, there are two reasons:

  1. Explicit command required — To trigger an approval, you need to post @coderabbitai approve as a top-level PR comment (not a reply in a thread). I only approve when explicitly instructed that way.

  2. Open security concerns — Even if the command were issued, I would flag that the PR has unresolved issues before approving:

    • 🔴 High: The default batchRequestLimit of -1 (unlimited) is a DoS risk for any deployment that doesn't explicitly configure the option.
    • 🟠 Medium: No concurrency cap — all items in a batch are dispatched simultaneously via Promise.allSettled, which can still exhaust DB connections/memory within the configured limit.
    • 🟠 Medium: Non-Parse.Error messages are returned verbatim to clients in per-item errors, bypassing the global sanitizer. (Based on a retrieved learning for this PR, tests were added to cover enableSanitizedErrorResponse behavior — worth confirming the implementation fully respects the flag for all non-Parse error cases.)
    • The PR's own checklist also notes missing documentation and a pending security review per contributing guidelines.

If you'd like to proceed with approval after addressing these, post @coderabbitai approve as a top-level comment and I'll take care of it.


🧠 Learnings used
Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-11-17T15:02:48.786Z
Learning: For Parse Server PRs, always suggest an Angular commit convention PR title that would make a meaningful changelog entry for developers. Update the PR title suggestion on every commit. The format should be: type(scope): description. Common types include feat, fix, perf, refactor, docs, test, chore. The scope should identify the subsystem (e.g., graphql, rest, push, security). The description should be action-oriented and clearly convey the change's impact to developers.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-11-17T15:02:24.824Z
Learning: For Parse Server PRs, always suggest an Angular-style PR title that would make a meaningful changelog entry for developers. Update the PR title suggestion with every new commit to the PR.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2026-03-23T22:11:01.903Z
Learning: In Parse Server PRs (parse-community/parse-server), when the commit/PR type prefix is `fix:`, the PR title must describe the bug (the broken behavior), not the fix (the expected or corrected behavior). For example, instead of "fix: Strip undefined values in ParseServerRESTController to match HTTP mode" (describes the fix), prefer "fix: directAccess update with undefined field values persists them as null in MongoDB" (describes the broken behavior).

Learnt from: Moumouls
Repo: parse-community/parse-server PR: 10341
File: spec/ParseGraphQLServer.spec.js:12265-12321
Timestamp: 2026-03-28T17:36:49.599Z
Learning: In parse-community/parse-server GraphQL bulk mutations, per-item failures must follow enableSanitizedErrorResponse: when true (default), a plain `Error` thrown from hooks is returned as `{ code: INTERNAL_SERVER_ERROR, message: 'Internal server error' }`; when false, the original `Error.message` is returned. Tests should cover both behaviors at the GraphQL layer.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-11-08T13:46:04.940Z
Learning: When reviewing Parse Server PRs that add new features, always check whether the feature is documented in the README.md file, though for new Parse Server options this is optional rather than required.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10149
File: src/Controllers/DatabaseController.js:1865-1873
Timestamp: 2026-03-08T21:51:35.204Z
Learning: In `src/Controllers/DatabaseController.js` (`performInitialization`), the `ensureAuthDataUniqueness` index creation is intentionally non-fatal (warn-only, no rethrow). Unlike username/email uniqueness which has been enforced since Parse Server's inception, authData uniqueness is new. Existing production databases may have duplicate authData entries caused by the race condition this PR fixes, so crashing on startup would be a breaking change. The application-level `ensureUniqueAuthDataId` check in `RestWrite.js` still runs as a fallback, and the warning log gives operators visibility to resolve duplicates manually before the index can be created successfully.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10320
File: spec/ParseGraphQLServer.spec.js:8661-8666
Timestamp: 2026-03-26T02:21:36.416Z
Learning: In parse-community/parse-server tests, swallowing errors with `.catch(() => {})` during schema cleanup is acceptable when the only expected failure is "class does not exist". Specifically, in spec/ParseGraphQLServer.spec.js beforeEach blocks that call `new Parse.Schema('SomeClass').purge()` and `.delete()`, using `.catch(() => {})` is an intentional pattern to keep test setup/teardown resilient and non-flaky. Do not flag this as an issue in future reviews.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-12-02T06:55:53.808Z
Learning: When reviewing Parse Server PRs that add or modify Parse Server options, always verify that changes are properly reflected in three files: src/Options/index.js (where changes originate), src/Options/Definitions.js, and src/Options/docs.js. The correct workflow is: make changes in index.js first, then run `npm run definitions` to automatically replicate the changes to Definitions.js and docs.js.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10161
File: src/Adapters/Storage/Postgres/PostgresStorageAdapter.js:1743-1750
Timestamp: 2026-03-09T21:11:01.966Z
Learning: In `src/Adapters/Storage/Postgres/PostgresStorageAdapter.js` (`updateObjectsByQuery`), the nested sub-key `c` (derived from dot-notation originalUpdate keys) is directly interpolated into SQL string literals in the Object Increment path (`->>'${c}'` and `CONCAT('{"${c}":', ...)`). This is a confirmed SQL injection vulnerability tracked as GHSA-gqpp-xgvh-9h7h and will be fixed in a separate dedicated PR. The maintainer (mtrezza) prefers a simpler fix — either single-quote escaping (`c.replace(/'/g, "''")`) or strict key name validation (e.g., `/^[A-Za-z0-9_]+$/`) — over restructuring with `jsonb_build_object`, to minimize SQL structure changes. Do not suggest `jsonb_build_object` for this fix.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10175
File: src/LiveQuery/ParseLiveQueryServer.ts:919-952
Timestamp: 2026-03-10T18:04:39.708Z
Learning: In Parse Server LiveQuery (`src/LiveQuery/ParseLiveQueryServer.ts`), the `addProtectedFields` method in `DatabaseController` uses **intersection** logic: it computes a protected set for each applicable ACL group (`*`, `userId`, `role:X`) and intersects them. Role entries serve as **exemptions** (they narrow/reduce the protected set for that role), not additions. Consequently, passing `userRoles: []` when building the `auth` object for the protected-fields WHERE-clause check is deliberately more restrictive (not less): without role sets, only the `'*'` set applies, which is the maximum protection. The only correctness gap is a rare role-only config (`{ 'role:X': ['field'] }` with no `'*'` entry), where omitting roles yields an empty intersection and the field is unprotected for the role member — but unprivileged users are unaffected, so this is not a security escalation. The same pattern exists pre-existing in `_filterSensitiveData`. This is intentional and documented with tests.

@Moumouls

Copy link
Copy Markdown
Member Author

@coderabbitai batchRequestLimit -1 is expected and will be changed in next major parse-server version

@Moumouls

Copy link
Copy Markdown
Member Author

@coderabbitai approve

coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 28, 2026
@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
✅ Actions performed

Comments resolved and changes approved.

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@Moumouls, understood — that's consistent with the existing requestComplexity opt-in policy for PS9, where limits become mandatory defaults in PS10. I'll retract the 🔴 High severity flag for the -1 default.

(。•ᴗ•。)🐇


✏️ Learnings added
Learnt from: Moumouls
URL: https://github.com/parse-community/parse-server/pull/10341

Timestamp: 2026-03-28T19:24:55.356Z
Learning: In parse-community/parse-server (PR `#10341`), `requestComplexity.batchRequestLimit` intentionally defaults to `-1` (unlimited) in PS9. This is by design: batch request limiting is opt-in in the current major version and will become a hard default (safe non-unlimited value) in PS10, consistent with the same opt-in policy already established for other `requestComplexity` sub-options (e.g., `includeLimit`). Do not flag the `-1` default as a security gap for PS9.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Moumouls
Repo: parse-community/parse-server PR: 10204
File: src/Security/CheckGroups/CheckGroupServerConfig.js:145-147
Timestamp: 2026-03-14T21:39:34.558Z
Learning: In Parse Server's `src/Security/CheckGroups/CheckGroupServerConfig.js`, the "Request complexity limits enabled" security check intentionally uses `return` (not `throw 1`) when `requestComplexity` is `undefined` in PS9. This is by design: `requestComplexity` is opt-in in the current major version and will only become a hard-failing security check (throw 1 when undefined) in PS10. The deprecation DEPPS18 in `src/Deprecator/Deprecations.js` communicates this upcoming default change to operators.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10149
File: src/Controllers/DatabaseController.js:1865-1873
Timestamp: 2026-03-08T21:51:35.204Z
Learning: In `src/Controllers/DatabaseController.js` (`performInitialization`), the `ensureAuthDataUniqueness` index creation is intentionally non-fatal (warn-only, no rethrow). Unlike username/email uniqueness which has been enforced since Parse Server's inception, authData uniqueness is new. Existing production databases may have duplicate authData entries caused by the race condition this PR fixes, so crashing on startup would be a breaking change. The application-level `ensureUniqueAuthDataId` check in `RestWrite.js` still runs as a fallback, and the warning log gives operators visibility to resolve duplicates manually before the index can be created successfully.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10175
File: src/LiveQuery/ParseLiveQueryServer.ts:919-952
Timestamp: 2026-03-10T18:04:39.708Z
Learning: In Parse Server LiveQuery (`src/LiveQuery/ParseLiveQueryServer.ts`), the `addProtectedFields` method in `DatabaseController` uses **intersection** logic: it computes a protected set for each applicable ACL group (`*`, `userId`, `role:X`) and intersects them. Role entries serve as **exemptions** (they narrow/reduce the protected set for that role), not additions. Consequently, passing `userRoles: []` when building the `auth` object for the protected-fields WHERE-clause check is deliberately more restrictive (not less): without role sets, only the `'*'` set applies, which is the maximum protection. The only correctness gap is a rare role-only config (`{ 'role:X': ['field'] }` with no `'*'` entry), where omitting roles yields an empty intersection and the field is unprotected for the role member — but unprivileged users are unaffected, so this is not a security escalation. The same pattern exists pre-existing in `_filterSensitiveData`. This is intentional and documented with tests.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2026-03-23T22:11:01.903Z
Learning: In Parse Server PRs (parse-community/parse-server), when the commit/PR type prefix is `fix:`, the PR title must describe the bug (the broken behavior), not the fix (the expected or corrected behavior). For example, instead of "fix: Strip undefined values in ParseServerRESTController to match HTTP mode" (describes the fix), prefer "fix: directAccess update with undefined field values persists them as null in MongoDB" (describes the broken behavior).

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-11-08T13:46:04.940Z
Learning: When reviewing Parse Server PRs that add new features, always check whether the feature is documented in the README.md file, though for new Parse Server options this is optional rather than required.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10149
File: src/Adapters/Storage/Mongo/MongoStorageAdapter.js:844-868
Timestamp: 2026-03-09T01:30:19.872Z
Learning: In parse-community/parse-server (PR `#10149`), `ensureAuthDataUniqueness(provider)` on both MongoStorageAdapter and PostgresStorageAdapter is called **only once at server startup** from `DatabaseController.performInitialization()` (src/Controllers/DatabaseController.js line 1867). It is NOT called on any request path. The request-path function is `RestWrite.prototype.ensureUniqueAuthDataId` (src/RestWrite.js), which performs a query-based duplicate check — these are two completely different functions. Memoization/Set caching on `ensureAuthDataUniqueness` is unnecessary; DB-level idempotency (MongoDB error codes 85/86; Postgres `IF NOT EXISTS`) is sufficient.

Learnt from: Moumouls
Repo: parse-community/parse-server PR: 10341
File: spec/ParseGraphQLServer.spec.js:12265-12321
Timestamp: 2026-03-28T17:36:49.599Z
Learning: In parse-community/parse-server GraphQL bulk mutations, per-item failures must follow enableSanitizedErrorResponse: when true (default), a plain `Error` thrown from hooks is returned as `{ code: INTERNAL_SERVER_ERROR, message: 'Internal server error' }`; when false, the original `Error.message` is returned. Tests should cover both behaviors at the GraphQL layer.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10320
File: spec/ParseGraphQLServer.spec.js:8661-8666
Timestamp: 2026-03-26T02:21:36.416Z
Learning: In parse-community/parse-server tests, swallowing errors with `.catch(() => {})` during schema cleanup is acceptable when the only expected failure is "class does not exist". Specifically, in spec/ParseGraphQLServer.spec.js beforeEach blocks that call `new Parse.Schema('SomeClass').purge()` and `.delete()`, using `.catch(() => {})` is an intentional pattern to keep test setup/teardown resilient and non-flaky. Do not flag this as an issue in future reviews.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10308
File: src/Adapters/Storage/Postgres/PostgresStorageAdapter.js:1995-1998
Timestamp: 2026-03-24T23:49:11.335Z
Learning: In parse-community/parse-server's `PostgresStorageAdapter.js`, the `find`, `count`, and `distinct` methods all broadly suppress `PostgresMissingColumnError` (code `42703`) and return empty results (`[]` or `0`) rather than throwing. This is intentional design: the storage adapter should not know about specific Parse field semantics (e.g., `_tombstone`), and the pattern is consistent with how `PostgresRelationDoesNotExistError` (missing table → `[]`) is already handled. Do not suggest scoping the `42703` suppression to specific field names or query shapes — that would violate separation of concerns and break future internal fields that lack a physical Postgres column.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10130
File: src/RestQuery.js:890-910
Timestamp: 2026-03-07T22:42:53.791Z
Learning: In Parse Server's RestQuery.js, `validateIncludeComplexity` intentionally counts raw `this.include.length` (not deduplicated paths) for the `includeCount` limit. This is by design: the limit caps total request complexity, not unique paths. Deduplicating before the check would weaken the protection. Note: user-supplied include strings are already deduplicated at construction via `pathSet`, but `handleIncludeAll` may add array references with reference-inequality duplicates — that is a pre-existing concern separate from this security check.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-12-02T08:00:20.138Z
Learning: For Parse Server 9 release (PR `#9938` and related), the parse/push-adapter dependency must be upgraded to version >= 8.0.0, not 7.0.0. Version 8.x drops support for Node 18.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-12-02T08:00:08.440Z
Learning: For Parse Server 9 release preparation, the parse/push-adapter dependency should be upgraded to version >= 8.0.0, not 7.x, as version 8.x is required despite dropping Node 18 support (which aligns with Parse Server 9's removal of EOL Node versions).

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 10082
File: package.json:3-3
Timestamp: 2026-02-28T15:22:57.025Z
Learning: In the parse-community/parse-server repository, manual changes to the npm package version in package.json should never be approved. The package version is managed and bumped automatically by semantic-release during the release workflow.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-11-17T15:02:48.786Z
Learning: For Parse Server PRs, always suggest an Angular commit convention PR title that would make a meaningful changelog entry for developers. Update the PR title suggestion on every commit. The format should be: type(scope): description. Common types include feat, fix, perf, refactor, docs, test, chore. The scope should identify the subsystem (e.g., graphql, rest, push, security). The description should be action-oriented and clearly convey the change's impact to developers.

@mtrezza

mtrezza commented Apr 5, 2026

Copy link
Copy Markdown
Member
  • Concurrent vs sequential execution model: REST batch (/batch) runs sub-requests sequentially, but these GraphQL bulk mutations use Promise.allSettled to run all items concurrently. This is a meaningful behavioral difference — concurrent writes can cause unexpected results when items interact (e.g., triggers that query recently-created objects), higher peak DB load, and different failure ordering. The choice is reasonable for performance, but the difference should be documented in the PR description or a code comment so consumers know the contract.

  • Inconsistent per-item cloning: createMany deep-clones each fieldSet per item via cloneArgs({ fields: fieldSet }) inside the map (line ~438), while updateMany uses updateEntry.fields directly (line ~589). Since cloneArgs(args) at the top of each handler already deep-clones the entire input, the extra clone in createMany is redundant. Either remove it from createMany or add it to updateMany for consistency.

  • Mixed error construction in assertBulkInputLength: Empty input uses createSanitizedError(...) (respects enableSanitizedErrorResponse), but limit-exceeded uses new Parse.Error(...) directly. Can this be sanitized as well? Details logged server side; clients only get minimal information.

@mtrezza mtrezza changed the title feat: Create, update, delete many graphql feat(graphql): Add bulk mutations createMany, updateMany, deleteMany Apr 5, 2026
@mtrezza mtrezza changed the title feat(graphql): Add bulk mutations createMany, updateMany, deleteMany feat: Add GraphQL bulk mutations createMany, updateMany, deleteMany Apr 5, 2026
Avoid disclosing batchRequestLimit when enableSanitizedErrorResponse is on, and prevent shared field objects from being mutated across concurrent updateMany items.
@Moumouls

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
spec/ParseGraphQLServer.spec.js (1)

12516-12588: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse the two near-identical bootstrap helpers into one parameterized helper.

reconfigureGraphQLWithBatchLimit2AndOpenClient and reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient differ only in the reconfigureServer options; everything after that (Apollo link/client construction, schema recreation, CLP grant) is byte-identical. The same block is inlined a third time at Lines 12701-12734. Three copies of this sequence means any future change to the client setup has to be made in three places.

♻️ Proposed consolidation
-          async function reconfigureGraphQLWithBatchLimit2AndOpenClient() {
-            parseServer = await global.reconfigureServer({
-              requestComplexity: { batchRequestLimit: 2 },
-            });
+          async function reconfigureGraphQLAndOpenClient(serverOptions) {
+            parseServer = await global.reconfigureServer(serverOptions);
             await createGQLFromParseServer(parseServer);
             const httpLink = await createUploadLink({
               uri: 'http://localhost:13377/graphql',
               fetch,
               headers,
             });
             apolloClient = new ApolloClient({
               link: httpLink,
               cache: new InMemoryCache(),
               defaultOptions: {
                 query: {
                   fetchPolicy: 'no-cache',
                 },
               },
             });
             const sc = new Parse.Schema('BulkTest');
             await sc.purge().catch(() => {});
             await sc.delete().catch(() => {});
             await sc.addString('title').save();
             await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
             await updateCLP(
               {
                 create: { '*': true },
                 find: { '*': true },
                 get: { '*': true },
                 update: { '*': true },
                 delete: { '*': true },
               },
               'BulkTest'
             );
           }
-
-          async function reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient() {
-            parseServer = await global.reconfigureServer({
-              maintenanceKey: 'test2',
-              maxUploadSize: '1kb',
-              enableSanitizedErrorResponse: false,
-            });
-            await createGQLFromParseServer(parseServer);
-            const httpLink = await createUploadLink({
-              uri: 'http://localhost:13377/graphql',
-              fetch,
-              headers,
-            });
-            apolloClient = new ApolloClient({
-              link: httpLink,
-              cache: new InMemoryCache(),
-              defaultOptions: {
-                query: {
-                  fetchPolicy: 'no-cache',
-                },
-              },
-            });
-            const sc = new Parse.Schema('BulkTest');
-            await sc.purge().catch(() => {});
-            await sc.delete().catch(() => {});
-            await sc.addString('title').save();
-            await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
-            await updateCLP(
-              {
-                create: { '*': true },
-                find: { '*': true },
-                get: { '*': true },
-                update: { '*': true },
-                delete: { '*': true },
-              },
-              'BulkTest'
-            );
-          }

Call sites then become reconfigureGraphQLAndOpenClient({ requestComplexity: { batchRequestLimit: 2 } }), reconfigureGraphQLAndOpenClient({ maintenanceKey: 'test2', maxUploadSize: '1kb', enableSanitizedErrorResponse: false }), and for Lines 12701-12734 reconfigureGraphQLAndOpenClient({ requestComplexity: { batchRequestLimit: 2 }, enableSanitizedErrorResponse: false }).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/ParseGraphQLServer.spec.js` around lines 12516 - 12588, Replace the
duplicate reconfigureGraphQLWithBatchLimit2AndOpenClient,
reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient, and the third inline setup
with one parameterized reconfigureGraphQLAndOpenClient helper that accepts
reconfigureServer options and retains the shared Apollo client, schema, cache,
and CLP setup. Update all three call sites to pass only their differing option
objects.
src/GraphQL/loaders/parseClassMutations.js (2)

88-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The allow*Many guard clauses are dead logic, and null is not treated as "unset".

Two things here, and I'll defend both:

  1. allowCreateMany = isCreateManyEnabled && (isCreateEnabled || createManyExplicit === true) reduces to isCreateManyEnabled in every branch. If createManyExplicit is undefined, then isCreateManyEnabled === isCreateEnabled and the right-hand clause is also isCreateEnabled; if it is true, the right-hand clause is true; if it is false, the left-hand clause already short-circuits. Same for update/delete. The extra clause only obscures the intent (bulk defaults to the single-op flag, explicit config always wins).

  2. ParseGraphQLController._validateClassConfig accepts createMany: null as a valid "not set" value, but here only undefined falls back to isCreateEnabled; a persisted null yields isCreateManyEnabled === null, silently disabling the bulk mutation while create stays enabled. Use a nullish check.

♻️ Proposed simplification
-  const isCreateManyEnabled = createManyExplicit !== undefined ? createManyExplicit : isCreateEnabled;
-  const isUpdateManyEnabled = updateManyExplicit !== undefined ? updateManyExplicit : isUpdateEnabled;
-  const isDeleteManyEnabled = deleteManyExplicit !== undefined ? deleteManyExplicit : isDestroyEnabled;
-  const allowCreateMany =
-    isCreateManyEnabled && (isCreateEnabled || createManyExplicit === true);
-  const allowUpdateMany =
-    isUpdateManyEnabled && (isUpdateEnabled || updateManyExplicit === true);
-  const allowDeleteMany =
-    isDeleteManyEnabled && (isDestroyEnabled || deleteManyExplicit === true);
+  const allowCreateMany = createManyExplicit ?? isCreateEnabled;
+  const allowUpdateMany = updateManyExplicit ?? isUpdateEnabled;
+  const allowDeleteMany = deleteManyExplicit ?? isDestroyEnabled;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GraphQL/loaders/parseClassMutations.js` around lines 88 - 102, In the
mutation enablement setup, update createManyExplicit, updateManyExplicit, and
deleteManyExplicit to treat both null and undefined as unset when deriving their
is*ManyEnabled values, then simplify allowCreateMany, allowUpdateMany, and
allowDeleteMany to use those derived flags directly. Preserve explicit
true/false overrides and the existing single-operation defaults.

449-463: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

All three bulk resolvers recompute query-AST-derived selection data once per item. getFieldNames(mutationInfo), the prefix filter/map, extractKeysAndInclude, and (where used) needToGetAllKeys depend only on mutationInfo and the class schema, never on the item — so an N-item batch performs N identical AST walks inside the Promise.allSettled closures. Hoist them above the .map() in each resolver and keep only the genuinely per-item getOnlyRequiredFields(fields, ...) call inside.

  • src/GraphQL/loaders/parseClassMutations.js#L449-L463: hoist selectedFields, keys, include, and needToGetAllKeys above the createMany Promise.allSettled; also drop the redundant cloneArgs({ fields: fieldSet }) on Line 432 since Line 422 already deep-cloned args.
  • src/GraphQL/loaders/parseClassMutations.js#L608-L622: hoist the same four values above the updateMany Promise.allSettled.
  • src/GraphQL/loaders/parseClassMutations.js#L743-L746: hoist selectedFields, keys, and include above the deleteMany Promise.allSettled.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GraphQL/loaders/parseClassMutations.js` around lines 449 - 463, In
src/GraphQL/loaders/parseClassMutations.js#L449-L463, hoist selectedFields,
keys, include, and needToGetAllKeys above the createMany Promise.allSettled map,
leaving only getOnlyRequiredFields(fields, ...) per item; also remove the
redundant cloneArgs({ fields: fieldSet }) noted at `#L432`. Apply the same
hoisting in `#L608-L622` for updateMany, and hoist selectedFields, keys, and
include above the deleteMany Promise.allSettled in `#L743-L746`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spec/ParseGraphQLController.spec.js`:
- Around line 1073-1119: Replace the three toBeRejected assertions in the
updateGraphQLConfig validation tests with awaited
expectAsync(...).toBeRejectedWithError() calls, preserving each expected
invalid-alias message for createManyAlias, updateManyAlias, and deleteManyAlias.

In `@src/Error.js`:
- Around line 72-76: Update bulkErrorPayloadFromReason so the Parse.Error branch
returns reason.code and reason.message directly instead of calling
createSanitizedError. Preserve sanitization for the non-Parse error branch, and
update the affected Utils specification to expect the original Parse.Error
message.

In `@src/GraphQL/loaders/parseClassMutations.js`:
- Around line 429-430: Replace the unbounded Promise.allSettled fan-out in the
createMany flow around fieldsList, and apply the same bounded-concurrency
approach to updateMany and deleteMany. Process items through a small worker pool
or equivalent concurrency-limited index loop, preserving each operation’s
settled-result handling while ensuring the number of simultaneous database
mutations stays capped independently of requestComplexity.batchRequestLimit.
- Around line 589-592: Update the destructuring in the mutation update-entry
handling near updateEntry so fields is declared with const rather than let,
while retaining let for id because it is reassigned. Remove the redundant
cloneArgs call when deriving fields, since updates was already deep-cloned by
the earlier cloneArgs invocation.

---

Nitpick comments:
In `@spec/ParseGraphQLServer.spec.js`:
- Around line 12516-12588: Replace the duplicate
reconfigureGraphQLWithBatchLimit2AndOpenClient,
reconfigureGraphQLWithUnsanitizedErrorsAndOpenClient, and the third inline setup
with one parameterized reconfigureGraphQLAndOpenClient helper that accepts
reconfigureServer options and retains the shared Apollo client, schema, cache,
and CLP setup. Update all three call sites to pass only their differing option
objects.

In `@src/GraphQL/loaders/parseClassMutations.js`:
- Around line 88-102: In the mutation enablement setup, update
createManyExplicit, updateManyExplicit, and deleteManyExplicit to treat both
null and undefined as unset when deriving their is*ManyEnabled values, then
simplify allowCreateMany, allowUpdateMany, and allowDeleteMany to use those
derived flags directly. Preserve explicit true/false overrides and the existing
single-operation defaults.
- Around line 449-463: In src/GraphQL/loaders/parseClassMutations.js#L449-L463,
hoist selectedFields, keys, include, and needToGetAllKeys above the createMany
Promise.allSettled map, leaving only getOnlyRequiredFields(fields, ...) per
item; also remove the redundant cloneArgs({ fields: fieldSet }) noted at `#L432`.
Apply the same hoisting in `#L608-L622` for updateMany, and hoist selectedFields,
keys, and include above the deleteMany Promise.allSettled in `#L743-L746`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 14ede6bd-b63e-4add-b2af-63d3124ae7b6

📥 Commits

Reviewing files that changed from the base of the PR and between 5838c07 and 05ead2f.

📒 Files selected for processing (11)
  • spec/ParseGraphQLController.spec.js
  • spec/ParseGraphQLSchema.spec.js
  • spec/ParseGraphQLServer.spec.js
  • spec/Utils.spec.js
  • src/Controllers/ParseGraphQLController.js
  • src/Error.js
  • src/GraphQL/loaders/defaultGraphQLTypes.js
  • src/GraphQL/loaders/parseClassMutations.js
  • src/GraphQL/loaders/parseClassTypes.js
  • src/batch.js
  • src/batchRequestLimit.js

Comment on lines +1073 to +1119
expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className,
mutation: {
createMany: true,
createManyAlias: true,
},
},
],
})
).toBeRejected(
`Invalid graphQLConfig: classConfig:${className} is invalid because "mutation.createManyAlias" must be a string`
);

expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className,
mutation: {
updateMany: true,
updateManyAlias: 1,
},
},
],
})
).toBeRejected(
`Invalid graphQLConfig: classConfig:${className} is invalid because "mutation.updateManyAlias" must be a string`
);

expectAsync(
parseGraphQLController.updateGraphQLConfig({
classConfigs: [
{
className,
mutation: {
deleteMany: true,
deleteManyAlias: { not: 'valid' },
},
},
],
})
).toBeRejected(
`Invalid graphQLConfig: classConfig:${className} is invalid because "mutation.deleteManyAlias" must be a string`
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Jasmine expectAsync toBeRejected toBeRejectedWithError signature arguments

💡 Result:

In the Jasmine testing framework, expectAsync is used to create an asynchronous expectation, which returns a promise that must be awaited or returned by the spec [1]. The toBeRejected matcher expects a promise to be rejected, and it takes no arguments [2][3]. The toBeRejectedWithError matcher expects a promise to be rejected with a value that matches the provided parameters [4][5]. It accepts two optional arguments: 1. expected: An Error constructor that the thrown object should be an instance of [4][5]. If this argument is omitted, the default Error constructor is used [4][5]. 2. message: A string or regular expression that the message property of the thrown error must match [4][5]. Example usage: await expectAsync(aPromise).toBeRejectedWithError(MyCustomError, 'Error message'); await expectAsync(aPromise).toBeRejectedWithError(MyCustomError, /Error message/); await expectAsync(aPromise).toBeRejectedWithError(MyCustomError); await expectAsync(aPromise).toBeRejectedWithError('Error message');

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching spec/controller:"
git ls-files | rg '(^|/)(ParseGraphQLController\.spec\.js|ParseGraphQLController\.(js|ts))$' || true

file="spec/ParseGraphQLController.spec.js"
if [ -f "$file" ]; then
  echo
  echo "Relevant lines in $file:"
  sed -n '1050,1130p' "$file" | nl -ba -v1050
  echo
  echo "Search for toBeRejected/toBeRejectedWithError usages in spec:"
  rg -n "toBeRejected|toBeRejectedWithError|updateGraphQLConfig" "$file" || true
fi

echo
echo "Package/Jasmine context clues:"
git ls-files | rg '(^package\.json$|^package-lock\.json$|^yarn\.lock$|^pnpm-lock\.yaml$)' || true
if [ -f package.json ]; then
  node -e 'const p=require("./package.json"); console.log(JSON.stringify({dependencies:p.dependencies, devDependencies:p.devDependencies}, null, 2))'
fi

Repository: parse-community/parse-server

Length of output: 384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="spec/ParseGraphQLController.spec.js"

echo "Relevant lines in $file:"
sed -n '1050,1130p' "$file"

echo
echo "Search for toBeRejected/toBeRejectedWithError usages in spec:"
rg -n "toBeRejected|toBeRejectedWithError|updateGraphQLConfig" "$file" || true

echo
echo "Package/Jasmine context clues:"
git ls-files | rg '(^package\.json$|^package-lock\.json$|^yarn\.lock$|^pnpm-lock\.yaml$)' || true
if [ -f package.json ]; then
  node -e 'const p=require("./package.json"); console.log(JSON.stringify({dependencies:p.dependencies, devDependencies:p.devDependencies}, null, 2))'
fi

Repository: parse-community/parse-server

Length of output: 11321


Replace these rejected-message assertions with toBeRejectedWithError() and await them.

toBeRejected() does not accept an expected argument, so Jasmine ignores these strings and the assertions would pass even if the controller emitted a different error text. Also invoke await expectAsync(...) so a non-rejection is reported as a failed spec rather than an unhandled rejection.

💚 Proposed fix
-      expectAsync(
+      await expectAsync(
         parseGraphQLController.updateGraphQLConfig({
           classConfigs: [
             {
               className,
               mutation: {
                 createMany: true,
                 createManyAlias: true,
               },
             },
           ],
         })
-      ).toBeRejected(
+      ).toBeRejectedWithError(
         `Invalid graphQLConfig: classConfig:${className} is invalid because "mutation.createManyAlias" must be a string`
       );

Apply the same change to the updateManyAlias and deleteManyAlias cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/ParseGraphQLController.spec.js` around lines 1073 - 1119, Replace the
three toBeRejected assertions in the updateGraphQLConfig validation tests with
awaited expectAsync(...).toBeRejectedWithError() calls, preserving each expected
invalid-alias message for createManyAlias, updateManyAlias, and deleteManyAlias.

Comment thread src/Error.js
Comment on lines +72 to +76
function bulkErrorPayloadFromReason(reason, config) {
if (reason instanceof Parse.Error) {
const sanitized = createSanitizedError(reason.code, reason.message, config);
return { code: sanitized.code, message: sanitized.message };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Parse.Error per-item failures are rewritten to 'Permission denied', which the single-item mutations never do.

createSanitizedError is called with its default sanitizedMessage, so with enableSanitizedErrorResponse at its default every per-item Parse error message becomes 'Permission denied' — including OBJECT_NOT_FOUND, VALIDATION_ERROR raised by a beforeSave hook, or a duplicate-username error. The single-object createX/updateX/deleteX mutations pass Parse.Error straight through parseGraphQLSchema.handleError, so the same failure yields an actionable message there and a misleading one in the bulk variant.

Parse.Error messages are already author-controlled and are the established client contract; sanitization exists for internal leaks, which the non-Parse branch below already covers. I'd keep reason.message for Parse.Error and drop the sanitization on this branch (the spec at spec/Utils.spec.js Lines 293-307 encodes the current behavior and would need updating with it).

🐛 Proposed fix
 function bulkErrorPayloadFromReason(reason, config) {
   if (reason instanceof Parse.Error) {
-    const sanitized = createSanitizedError(reason.code, reason.message, config);
-    return { code: sanitized.code, message: sanitized.message };
+    return { code: reason.code, message: reason.message };
   }
📝 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
function bulkErrorPayloadFromReason(reason, config) {
if (reason instanceof Parse.Error) {
const sanitized = createSanitizedError(reason.code, reason.message, config);
return { code: sanitized.code, message: sanitized.message };
}
function bulkErrorPayloadFromReason(reason, config) {
if (reason instanceof Parse.Error) {
return { code: reason.code, message: reason.message };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Error.js` around lines 72 - 76, Update bulkErrorPayloadFromReason so the
Parse.Error branch returns reason.code and reason.message directly instead of
calling createSanitizedError. Preserve sanitization for the non-Parse error
branch, and update the affected Utils specification to expect the original
Parse.Error message.

Comment on lines +429 to +430
const settled = await Promise.allSettled(
fieldsList.map(fieldSet =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Unbounded fan-out: every item in the batch hits the database simultaneously.

Promise.allSettled over the full input list means a single mutation with N items issues N concurrent createObject calls (each of which may trigger beforeSave/afterSave and extra getObject fetches). With requestComplexity.batchRequestLimit defaulting to -1, N is client-controlled, so one request can exhaust the connection pool and starve unrelated traffic. Note this also diverges from the REST batch endpoint, which processes sequentially.

I'd recommend a bounded concurrency window (a small worker-pool over the index range) rather than relying solely on operators setting batchRequestLimit. Same pattern applies to updateMany (Line 586) and deleteMany (Line 739).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GraphQL/loaders/parseClassMutations.js` around lines 429 - 430, Replace
the unbounded Promise.allSettled fan-out in the createMany flow around
fieldsList, and apply the same bounded-concurrency approach to updateMany and
deleteMany. Process items through a small worker pool or equivalent
concurrency-limited index loop, preserving each operation’s settled-result
handling while ensuring the number of simultaneous database mutations stays
capped independently of requestComplexity.batchRequestLimit.

Comment thread src/GraphQL/loaders/parseClassMutations.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.