parse-server icon indicating copy to clipboard operation
parse-server copied to clipboard

insertMany and updateMany support

Open SebC99 opened this issue 6 years ago • 22 comments

Is there a plan to support insertMany and updateMany in Parse? It would increase a lot the performance of batch operations, either to create new objects or to update some existing objects. 2 uses cases would be:

  • to clone a certain amount of data, by creating 1000 objects at once. For now, saving those 1000 new objects is very very long, even with direct access, as the server starts one database insert op per object.
  • to increment a property in 1000 objects at once (or to update a specific field with the same value). For now, the server updates each object one by one, even if the update command is strictly the same.

I have no idea how to implement that so it would be hard to try to propose a PR, but I can see the updateMany is already in the mongo adapter as it's used by the push controller to update the pushStatus of the sent pushes.

SebC99 avatar Oct 22 '19 16:10 SebC99

I believe the limitation is the beforeSave hook; Parse allows its developers to validate the data before inserting to the DB. We would have to craft a scenario where the beforeSave is either skipped, or runs once, separately for a batch request, for example a beforeSaveAll. An RFC with problems and alternatives would be needed before something like this is added.

omairvaiyani avatar Oct 23 '19 10:10 omairvaiyani

IIRC updateMany does not return which objects have been modified. I cannot think of a way around this that would be scalable and maintain the performance gain of updateMany vs. updating individual objects.

So I think it can only be collective hooks like beforeSaveAll as @omairvaiyani suggested. Or no hook at all (initially), which would also be justified because it would still be the cheapest way to update multiple objects.

mtrezza avatar Oct 28 '19 01:10 mtrezza

Just to give more details, directly using the existing updateMany method of the mongo Adapter results in 0.5s save for 5000 objects instead of 6seconds with the usual parse saveall method... Of course I skipped the beforeSave, various controls, etc. but still, the gain could be huge. One way would also be to activate it only if no declared beforeSave?

SebC99 avatar Oct 28 '19 07:10 SebC99

I think updateMany has to be an additional method, it is unlikely that it can increase performance of an existing method.

That’s because updateMany only applies to a specific use case: apply exactly the same modification to objects that are selected by a query, without returning those objects.

Parse SDKs currently have no comparable concept of mass modifying objects that are selected based on a query, in one single step. For example, currently a query fetches objects, you loop through and modify each object, and call saveAll. We would have to determine if the same manipulation has been done to each object of the query to use updateMany, and whether no objects have been added or removed from the list of object we want to update. That doesn't look feasible to me.

So if we decide to implement updateMany, there would be no performance improvement for existing deployments. You’d have to identify queries that are fetching objects, modify them in exactly the same way without any conditional logic, and save them. Only these queries can be replaced with the new updateMany method to improve performance.

A updateMany method will have to bypass any before/after hooks, because the objects are not returned and even if we fetch the objects in an extra step after modifying them, we’d have to apply a collection lock to ensure they have not been modified between the updateMany and find operation. That’s not feasible performance wise.

I don’t see a reasonable use case for a generic beforeSaveAll hook. It defies the purpose of a hook as all we know is that an updateMany command will be executed on the collection.

The PR should be straightforward: add a new method for ParseQuery that takes a modification definition object, which you pass directly to the updateMany command.

let query = new ParseQuery(“_User”);
query.equalTo(“blocked”, true);
let update = { “$set”: { “blocked”: false }};
query.updateAll(update, { useMasterKey: true });

Sent with GitHawk

mtrezza avatar Oct 28 '19 11:10 mtrezza

I would add a saveAll case which would be an insertMany and not an updateMany'. And a deleteMany` too, as it would provide a way to remove and replace multiple objects at the same time allowing for much faster saving.

SebC99 avatar Nov 06 '19 23:11 SebC99

@SebC99 Sounds good, do you have a suggestion for the syntax? Since we already have a saveAll.

mtrezza avatar Nov 07 '19 00:11 mtrezza

The label type:feature cannot be used in combination with type:improvement.

Is there any ongoing work on this?

Pushpit07 avatar Nov 13 '22 19:11 Pushpit07

Not to my knowledge. In our case, we use a modified MongoStorageAdapter which includes a updateObjectsByBulk method but as we need to define the update objects ourselves, we could have directly used the mongo driver to do the same.

SebC99 avatar Nov 13 '22 19:11 SebC99

I think this would be a useful feature addition. It doesn't look that complicated to add, as it's just passing everything through to the MongoDB adapter. Anyone wants to give a try to implement this?

mtrezza avatar Nov 13 '22 20:11 mtrezza

Our implementation looks like this:

updateObjectsByBulk(
    className: string,
    schema: SchemaType,
    operations: any,
    transactionalSession: ?any
  ) {
    schema = convertParseSchemaToMongoSchema(schema);
    const bulks = operations.map(({updateOne, updateMany, insertOne}) => {
      return updateOne ? {
        updateOne: {
          filter: transformWhere(className, updateOne.filter, schema),
          update: transformUpdate(className, updateOne.update, schema),
          upsert: false
        }
      } : updateMany ? {
        updateMany: {
          filter: transformWhere(className, updateMany.filter, schema),
          update: transformUpdate(className, updateMany.update, schema),
          upsert: false
        }
      } : {
        insertOne: {
          document: parseObjectToMongoObjectForCreate(className, insertOne.document, schema)
        }
      };
    });
    return this._adaptiveCollection(className)
      .then(collection =>
        collection._mongoCollection.bulkWrite(bulks, {
          session: transactionalSession || undefined,
          ordered: false,
          bypassDocumentValidation: true,
          writeConcern: {w: 0, j: false}
        })
      )
      .then(result => mongoObjectToParseObject(className, result.value, schema))
      .catch(error => {
        if (error.code === 11000) {
          throw new Parse.Error(
            Parse.Error.DUPLICATE_VALUE,
            'A duplicate value for a field with unique values was provided'
          );
        }
        throw error;
      })
      .catch(err => this.handleError(err));
  }

But then, as I said, we have to define the update operations ourselves, which is not as convenient as standard Parse.Object updates.

SebC99 avatar Nov 13 '22 20:11 SebC99

On this one, @mtrezza, after reading your comments, I understand that the correct implementation is to bypass any hooks. However, could this be an issue if we have hooks that add custom logic to an object before creation?

coratgerl avatar Nov 21 '25 14:11 coratgerl

@coratgerl This feature should be just “sugar on top” of the existing API, because for insert operations and many others we still need to trigger hooks and security checks normally for each provided object.

From my point of view, it’s simply an endpoint that will internally call RestQuery in parallel for each provided item, nothing more.

The only benefits I see are:

  • A single HTTP call to the same server with multiple objects, which can reduce some HTTP transport overhead.
  • (Not in favor of) The possibility of adding a transactional feature, but since hooks allow developers to implement anything in before/after save, Parse Server isn’t really ready for this. It could also create confusion for developers when objects do or do not get rolled back.

Moumouls avatar Nov 21 '25 14:11 Moumouls

createMany will be just a tiny proxy layer

Moumouls avatar Nov 21 '25 14:11 Moumouls

hold on, there is something super complex here, about the case some objects creation fails, what is is correct status code for partial object creation ? what happens if all fails ?

Because in some beforeSave hooks some object could be refused, currently it's unclear, and all edge case should be investigated here before implementation, to have a clear spec

how the JS SDK handle this, how to tell to the SDK that some object succeed and others failed ?

Moumouls avatar Nov 21 '25 14:11 Moumouls

btw, there is no perf improvement to get here

Moumouls avatar Nov 21 '25 14:11 Moumouls

what is is correct status code for partial object creation ? what happens if all fails ?

We don't need to decide this, I think; it could return an array of promise results and we add an option for fail all on first fail, or keep trying. Similar to JS Promise.all and its variants.

there is no perf improvement to get here

Bypassing of hooks can be useful, that's the real power of *many DB ops. We are already exposing some more complex methods via Parse.Server.database like upsert with query restrictions. Not sure if that bypasses hooks, but we could add it to that namespace to make it only available in CloudCode and therefore skip some performance hurdles.

mtrezza avatar Nov 21 '25 15:11 mtrezza

Bypassing hooks should be an option (and only available with the master key).

However, developers will need to use it carefully, because if they use hooks or perform ACL manipulations during before save, all those security settings and checks will be bypassed. This could eventually lead to objects becoming publicly accessible to all users, or to objects not being accessible at all.

Hooks are run at the RestWrite/RestQuery level, so I believe they are skipped in Parse.Server.database.

@coderabbitai can you create a quick diagram about when Parse hooks, security checks are done in parse-server ? does Parse.Server.database is just the controller layer for the DB without any hooks triggers ?

Moumouls avatar Nov 22 '25 09:11 Moumouls

[!TIP] For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find RestWrite and RestQuery files
fd -e js "RestWrite" --type f

# Find RestQuery
fd -e js "RestQuery" --type f

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Search for Parse.Server.database implementation
rg -n -C5 "Parse\.Server\.database" --type js

# Also search for ParseServer.database
rg -n -C5 "ParseServer\.database" --type js

Length of output: 5640


🏁 Script executed:

#!/bin/bash
# Find DatabaseController
fd -e js "DatabaseController" --type f

# Look at the structure
cat src/Controllers/DatabaseController.js | head -100

Length of output: 3032


🏁 Script executed:

#!/bin/bash
# Search for where hooks are triggered - looking for beforeSave, afterSave, etc.
rg -n -C3 "maybeRunTrigger|maybeRunAfterFindTrigger|beforeSave|afterSave" src/RestWrite.js | head -80

Length of output: 2614


🏁 Script executed:

#!/bin/bash
# Check ACL validation in RestWrite
rg -n -C3 "validatePermission|canAddField|ACL" src/RestWrite.js | head -80

Length of output: 2572


🏁 Script executed:

#!/bin=bash
# Look at RestQuery for hooks
rg -n -C3 "maybeRunTrigger|afterFind" src/RestQuery.js | head -60

Length of output: 1265


🏁 Script executed:

#!/bin/bash
# Check what ParseServer class exposes
rg -n -C5 "class ParseServer" src/ParseServer.js | head -40

# Look for database property
rg -n -C3 "this\.database|get database" src/ParseServer.js | head -50

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Look at triggers.js to understand hook execution
cat src/triggers.js | head -150

Length of output: 4990


🏁 Script executed:

#!/bin/bash
# Find where RestWrite and RestQuery are instantiated from API routes
rg -n -C3 "new RestWrite|new RestQuery" src/Routers/*.js | head -60

Length of output: 393


🏁 Script executed:

#!/bin/bash
# Look at the main router structure
ls -la src/Routers/

Length of output: 1694


🏁 Script executed:

#!/bin/bash
# Find the main ParseServer or index file
fd -e js "index" --max-depth 1 --type f
ls -la src/*.js | head -20

Length of output: 1360


🏁 Script executed:

#!/bin/bash
# Look at ClassesRouter to see how RestWrite/RestQuery are used
cat src/Routers/ClassesRouter.js

Length of output: 6872


🏁 Script executed:

#!/bin/bash
# Find where Parse.Server is defined/exported
rg -n "Parse\.Server\s*=" --type js
rg -n "module\.exports.*Server|export.*ParseServer" src/*.js | head -20

Length of output: 270


🏁 Script executed:

#!/bin/bash
# Look at DatabaseController methods to understand what it exposes
rg -n "^\s*async\s+\w+|^\s+\w+\(|^\s+async\s+\w+\(" src/Controllers/DatabaseController.js | head -80

Length of output: 2676


🏁 Script executed:

#!/bin/bash
# Check if there's a ParseServer.js file
fd "ParseServer.js" --type f
cat src/index.js | head -200

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Look at PublicAPIRouter to understand the REST API flow
cat src/Routers/PublicAPIRouter.js | head -150

Length of output: 3684


🏁 Script executed:

#!/bin/bash
# Find where security checks happen - look for validatePermission
rg -n "validatePermission" --type js src/Controllers/DatabaseController.js | head -20

Length of output: 409


🏁 Script executed:

#!/bin/bash
# Check SchemaController for permission checks
rg -n -C3 "validatePermission|CLP|classLevelPermission" src/Controllers/SchemaController.js | head -80

Length of output: 2823


🏁 Script executed:

#!/bin/bash
# Look at rest.js to understand the flow
cat src/rest.js | head -200

Length of output: 6275


🏁 Script executed:

#!/bin/bash
# Check how Parse.Server.database is exposed
fd "index.js" --type f src/ --max-depth 1
cat src/index.js 2>/dev/null || cat index.js | head -200

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Search for where database property is exposed on Parse.Server
rg -n "database.*DatabaseController|DatabaseController" --type js | grep -E "(export|Parse\.Server|module\.exports)" | head -30

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Look for the main Parse Server class export
fd "^ParseServer" --type f
cat src/ParseServer.ts 2>/dev/null || cat ParseServer.js 2>/dev/null || echo "Not found in expected location"

Length of output: 23064


🏁 Script executed:

#!/bin/bash
# Find all index or main entry files
find . -name "index.js" -o -name "index.ts" | grep -v node_modules | head -10

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Look at the root index to see exports
cat index.js 2>/dev/null | head -100

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check rest.js implementation
rg -n "export function|export const" src/rest.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at rest.js create, update, find implementations
cat src/rest.js

Length of output: 10144


🏁 Script executed:

#!/bin/bash
# Look at Parse.Server definition in cloud-code
cat src/cloud-code/Parse.Server.js 2>/dev/null || cat cloud-code/Parse.Server.js 2>/dev/null || find . -name "Parse.Server.js" -type f | head -1 | xargs cat

Length of output: 503


🏁 Script executed:

#!/bin/bash
# Check the Controllers index to see what's exported
cat src/Controllers/index.js | head -150

Length of output: 5484


🏁 Script executed:

#!/bin/bash
# Look for how database is exposed on Parse.Server
rg -n "database.*:" src/cloud-code/ --type js || rg -n "database" src/cloud-code/ --type js | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check where hooks are triggered in RestWrite - get full context
rg -n -B5 -A10 "runBeforeSaveTrigger|runAfterSaveTrigger" src/RestWrite.js | head -100

Length of output: 1980


🏁 Script executed:

#!/bin/bash
# Check where ACL checks happen in RestWrite execute flow
rg -n -B3 -A3 "getUserAndRoleACL|validateClientClass" src/RestWrite.js | head -60

Length of output: 808


🏁 Script executed:

#!/bin/bash
# Look at the full execute flow in RestWrite
sed -n '97,200p' src/RestWrite.js

Length of output: 2845


🏁 Script executed:

#!/bin/bash
# Check RestQuery execute method
rg -n -B5 -A20 "execute.*function|^.*execute\(" src/RestQuery.js | head -150

Length of output: 4715


@Moumouls, you're absolutely correct! Let me create a diagram to illustrate when hooks and security checks are executed in Parse Server:

graph TB
    subgraph "REST API Path (with Hooks & Full Security)"
        A[REST API Request] --> B[Router<br/>ClassesRouter, UsersRouter, etc.]
        B --> C[rest.js functions<br/>create, find, update, delete]
        C --> D{Operation Type}
        
        D -->|Write Operations| E[RestWrite]
        D -->|Read Operations| F[RestQuery]
        
        E --> E1[1. getUserAndRoleACL<br/>Build ACL list from user roles]
        E1 --> E2[2. validateClientClassCreation<br/>Check if class creation allowed]
        E2 --> E3[3. validateAuthData<br/>Validate auth providers]
        E3 --> E4["🎯 4. runBeforeSaveTrigger()<br/><b>HOOK: beforeSave/beforeDelete</b>"]
        E4 --> E5[5. validateSchema<br/>Check schema constraints]
        E5 --> E6[6. runDatabaseOperation<br/>→ DatabaseController]
        E6 --> E7["🎯 7. runAfterSaveTrigger()<br/><b>HOOK: afterSave/afterDelete</b>"]
        
        F --> F1[1. getUserAndRoleACL<br/>Build ACL list from user roles]
        F1 --> F2["🎯 2. beforeFind trigger<br/><b>HOOK: beforeFind</b><br/>(in rest.js)"]
        F2 --> F3[3. buildRestWhere<br/>Process query conditions]
        F3 --> F4[4. denyProtectedFields<br/>Apply protected fields]
        F4 --> F5[5. runFind<br/>→ DatabaseController]
        F5 --> F6["🎯 6. afterFind trigger<br/><b>HOOK: afterFind</b><br/>(in rest.js)"]
    end
    
    subgraph "DatabaseController (CLP & ACL Enforcement)"
        DC[DatabaseController]
        DC --> DC1[validateQuery<br/>Validate query structure]
        DC1 --> DC2[validatePermission<br/>Check CLP - Class Level Permissions]
        DC2 --> DC3[addReadACL / addWriteACL<br/>Apply ACL filters to query]
        DC3 --> DC4[transformAuthData<br/>Transform auth data]
        DC4 --> SA[StorageAdapter<br/>MongoDB / PostgreSQL]
    end
    
    subgraph "Direct Database Access (NO Hooks)"
        PSD["Parse.Server.database<br/>(CloudCode / Maintenance)"]
        PSD --> DC
    end
    
    E6 --> DC
    F5 --> DC
    
    style E4 fill:#ffcccc
    style E7 fill:#ffcccc
    style F2 fill:#ffcccc
    style F6 fill:#ffcccc
    style PSD fill:#ffffcc
    style DC2 fill:#ccffcc
    style DC3 fill:#ccffcc

Key Points:

1. Hooks are triggered at the RestWrite/RestQuery level:

  • beforeSave, afterSave, beforeDelete, afterDelete → RestWrite
  • beforeFind, afterFind → rest.js (before/after RestQuery)
  • NOT triggered in DatabaseController

2. Security checks happen at multiple levels:

RestWrite/RestQuery Level:

  • getUserAndRoleACL() - Builds ACL list from authenticated user and their roles
  • validateClientClassCreation() - Checks if non-master keys can create classes
  • validateAuthData() - Validates authentication providers
  • denyProtectedFields() - Applies protected fields configuration

DatabaseController Level:

  • validatePermission() - Enforces Class Level Permissions (CLP) for find/create/update/delete/addField
  • addReadACL() / addWriteACL() - Applies ACL filters to database queries (_rperm, _wperm)
  • validateQuery() - Prevents querying protected fields like ACL, _email_verify_token, etc.

3. Parse.Server.database bypasses hooks:

Yes, Parse.Server.database is the DatabaseController exposed for direct access in Cloud Code. As you correctly noted:

  • ✅ It SKIPS all hooks (beforeSave, afterSave, beforeFind, afterFind, etc.)
  • ✅ It STILL APPLIES CLP and ACL checks (unless using maintenance auth)
  • ✅ It's useful for administrative operations, migrations, and maintenance tasks

This is why in the test files you saw direct usage like:

await Parse.Server.database.update('_User', { objectId: user.id }, { ... })
await Parse.Server.database.find('_User', { objectId: user.id }, {}, Auth.maintenance(Parse.Server))

For the insertMany/updateMany feature you're discussing, bypassing hooks with an option (master key only) would indeed be similar to using Parse.Server.database directly, but developers need to be extremely careful as you mentioned—they would lose all hook-based validations, ACL manipulations, and security checks performed in those hooks.


🧠 Learnings used
Learnt from: EmpiDev
Repo: parse-community/parse-server PR: 9770
File: src/triggers.js:467-477
Timestamp: 2025-08-27T12:33:06.237Z
Learning: In the Parse Server codebase, maybeRunAfterFindTrigger is called in production with Parse.Query objects constructed via withJSON(), so the plain object query handling bug only affects tests, not production code paths.

Learnt from: EmpiDev
Repo: parse-community/parse-server PR: 9770
File: spec/CloudCode.spec.js:446-469
Timestamp: 2025-08-26T14:06:31.853Z
Learning: In the Parse Server codebase, when handling query objects in maybeRunAfterFindTrigger, objects without a where property that contain options like limit/skip should be treated as query JSON with an empty where clause using the spread pattern { where: {}, ...query }, not nested as { where: query }.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 9858
File: src/GraphQL/ParseGraphQLServer.js:176-178
Timestamp: 2025-09-21T15:43:32.265Z
Learning: The GraphQL playground feature in ParseGraphQLServer.js (applyPlayground method) is intended for development environments only, which is why it includes the master key in client-side headers.

coderabbitai[bot] avatar Nov 22 '25 09:11 coderabbitai[bot]

@coderabbitai, so when using *many ops via Parse.Server.database, are there any DB ops that need to be executed Parse Server side for each op? I assume not, because the *many ops are all concerning a single target collection and the CLP can be determined once.

mtrezza avatar Nov 22 '25 12:11 mtrezza

[!TIP] For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look at DatabaseController methods for create, update to understand per-object vs batch operations
rg -n -A30 "^\s*async create\(" src/Controllers/DatabaseController.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at update method in DatabaseController
rg -n -A30 "^\s*async update\(" src/Controllers/DatabaseController.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check how ACLs are added in write operations
rg -n -C5 "addWriteACL|_wperm|addReadACL|_rperm" src/Controllers/DatabaseController.js | head -100

Length of output: 3155


🏁 Script executed:

#!/bin/bash
# Look at validatePermission to understand CLP checks
rg -n -A20 "^\s*validatePermission\(" src/Controllers/DatabaseController.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's already any many operation implementation
rg -n "updateMany|insertMany|bulkWrite" src/Controllers/DatabaseController.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the full create method signature and implementation
sed -n '/async create(/,/^  }/p' src/Controllers/DatabaseController.js | head -100

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the update method implementation
sed -n '/async update(/,/^  }/p' src/Controllers/DatabaseController.js | head -100

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check MongoDB adapter for updateMany/insertMany implementations
rg -n "updateMany|insertMany|bulkWrite" src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Length of output: 188


🏁 Script executed:

#!/bin/bash
# Look at how CLP is validated in SchemaController
rg -n -A15 "validatePermission" src/Controllers/SchemaController.js | head -80

Length of output: 2424


🏁 Script executed:

#!/bin/bash
# Check if there are any batch operations already
rg -n "saveAll|destroyAll" src/Controllers/DatabaseController.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the DatabaseController create/update methods more carefully
rg -n "^\s+create\(|^\s+update\(|^\s+async create|^\s+async update" src/Controllers/DatabaseController.js

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Get the structure of create and update operations
ast-grep --pattern 'create($$$) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at how operations are processed - find the create method
grep -n "create(" src/Controllers/DatabaseController.js | head -10

Length of output: 13


🏁 Script executed:

#!/bin/bash
# Show lines around create method definition
sed -n '450,650p' src/Controllers/DatabaseController.js

Length of output: 6960


🏁 Script executed:

#!/bin/bash
# Check how updateMany is used in MongoStorageAdapter
rg -n -B5 -A10 "updateMany" src/Adapters/Storage/Mongo/MongoStorageAdapter.js | head -80

Length of output: 1456


🏁 Script executed:

#!/bin/bash
# Look at the find method to see how ACLs and CLPs are applied
rg -n -B3 -A20 "^\s+find\(className" src/Controllers/DatabaseController.js | head -80

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Show the create method
sed -n '827,1050p' src/Controllers/DatabaseController.js

Length of output: 7651


🏁 Script executed:

#!/bin/bash
# Look for the find method
rg -n "^\s+find\(" src/Controllers/DatabaseController.js

Length of output: 12


🏁 Script executed:

#!/bin/bash
# Show lines around the find method
sed -n '1200,1400p' src/Controllers/DatabaseController.js

Length of output: 7628


🏁 Script executed:

#!/bin/bash
# Check how CLP validation works - it should be per-class, not per-object
rg -n -B2 -A10 "testPermissions\(" src/Controllers/SchemaController.js

Length of output: 1283


@mtrezza, you're absolutely correct! When using *many operations via Parse.Server.database, there are no additional DB ops that need to be executed Parse Server side for each object.

Here's what happens:

1. CLP (Class Level Permissions) - Checked Once Per Operation

Looking at the DatabaseController code:

  • create: Line 856 calls schemaController.validatePermission(className, aclGroup, 'create')
  • update: Line 516 calls schemaController.validatePermission(className, aclGroup, 'update')
  • find: Line 1258 calls schemaController.validatePermission(className, aclGroup, op)

The SchemaController.testPermissions() method (line 1366) checks permissions against the class and the aclGroup (array of user/role IDs), not individual objects. So CLP validation is a single check per operation, exactly as you assumed.

2. ACL (Object-Level Access Control) - Applied as Query Filters

ACL checks are added directly to the MongoDB query, not evaluated per-object on the Parse Server side:

// Line 24-29: addWriteACL
function addWriteACL(query, acl) {
  const newQuery = _.cloneDeep(query);
  newQuery._wperm = { $in: [null, ...acl] };
  return newQuery;
}

// Line 31-36: addReadACL  
function addReadACL(query, acl) {
  const newQuery = _.cloneDeep(query);
  newQuery._rperm = { $in: [null, '*', ...acl] };
  return newQuery;
}

These ACL filters are applied in:

  • update operations: Line 545 query = addWriteACL(query, acl)
  • read operations: Line 1295 query = addReadACL(query, acl)

The ACL filtering happens at the database level through the _wperm and _rperm fields, so MongoDB handles the filtering in a single query.

3. updateMany Already Implemented

The MongoStorageAdapter already uses updateMany (line 582) in the updateObjectsByQuery method, which is called when many: true is passed to DatabaseController's update method (line 620).

Summary

For *many operations:

  • CLP: Single check per operation (class-level)
  • ACL: Database-level query filter (no per-object Parse Server ops)
  • Performance: Full native DB *many performance is achievable
  • Hooks: Bypassed (no per-object processing)

So when implementing insertMany/updateMany features, the performance gain is real and there's no Parse Server-side bottleneck requiring per-object database operations.


🧠 Learnings used
Learnt from: EmpiDev
Repo: parse-community/parse-server PR: 9770
File: src/triggers.js:467-477
Timestamp: 2025-08-27T12:33:06.237Z
Learning: In the Parse Server codebase, maybeRunAfterFindTrigger is called in production with Parse.Query objects constructed via withJSON(), so the plain object query handling bug only affects tests, not production code paths.

Learnt from: EmpiDev
Repo: parse-community/parse-server PR: 9770
File: spec/CloudCode.spec.js:446-469
Timestamp: 2025-08-26T14:06:31.853Z
Learning: In the Parse Server codebase, when handling query objects in maybeRunAfterFindTrigger, objects without a where property that contain options like limit/skip should be treated as query JSON with an empty where clause using the spread pattern { where: {}, ...query }, not nested as { where: query }.

Learnt from: RahulLanjewar93
Repo: parse-community/parse-server PR: 9744
File: spec/ParseLiveQuery.spec.js:0-0
Timestamp: 2025-04-30T19:31:35.344Z
Learning: In the Parse Server codebase, the functions in QueryTools.js are typically tested through end-to-end behavior tests rather than direct unit tests, even though the functions are exported from the module.

Learnt from: mtrezza
Repo: parse-community/parse-server PR: 9858
File: src/GraphQL/ParseGraphQLServer.js:176-178
Timestamp: 2025-09-21T15:43:32.265Z
Learning: The GraphQL playground feature in ParseGraphQLServer.js (applyPlayground method) is intended for development environments only, which is why it includes the master key in client-side headers.

coderabbitai[bot] avatar Nov 22 '25 12:11 coderabbitai[bot]

In conclusion I think a *many op only makes sense to be implemented on the Parse.Server.database level, because to add trigger executions, we effectively would have to deconstruct the *many op into single ops. Otherwise the trigger executions cannot be invoked in the sequentially correct order or at the correct point in time.

mtrezza avatar Nov 22 '25 12:11 mtrezza