Skip to content

Separated db-controller.js into modules#201

Merged
thehabes merged 9 commits intomainfrom
db-controller-bustamonte
Jul 8, 2025
Merged

Separated db-controller.js into modules#201
thehabes merged 9 commits intomainfrom
db-controller-bustamonte

Conversation

@cubap
Copy link
Copy Markdown
Member

@cubap cubap commented Jul 3, 2025

🎯 Refactor monolithic db-controller.js into modular controller structure

📋 Summary

This PR addresses the technical debt of the large, monolithic db-controller.js file by breaking it down into smaller, logically grouped modules. The refactoring improves code maintainability, readability, and follows separation of concerns principles.

🔄 Changes Made

New Controller Modules Created:

  • controllers/utils.js - Utility functions (idNegotiation, generateSlugId, etc.)
  • controllers/crud.js - CRUD operations (create, query, id)
  • controllers/delete.js - Delete operations and history helpers
  • controllers/update.js - Update operations aggregator
  • controllers/putUpdate.js - PUT update and import operations
  • controllers/patchUpdate.js - PATCH update operations
  • controllers/patchSet.js - PATCH set operations
  • controllers/patchUnset.js - PATCH unset operations
  • controllers/overwrite.js - Overwrite operations with optimistic locking
  • controllers/bulk.js - Bulk operations (bulkCreate, bulkUpdate)
  • controllers/history.js - History operations (since, history, head requests)
  • controllers/release.js - Release operations
  • controllers/gog.js - Gallery of Glosses endpoints

Updated Files:

  • db-controller.js - Now serves as a clean aggregator that imports and re-exports from new modules
  • routes/__tests__/overwrite-optimistic-locking.test.js - Added test for optimistic locking

Backup Files:

  • db-controller-backup.js - Complete backup of original file
  • db-controller.js.backup - Additional backup for safety

🎯 Key Benefits

  1. 📦 Modular Structure: Each controller has a single responsibility
  2. 🔧 Easier Maintenance: Smaller files are easier to understand and modify
  3. 🧪 Better Testability: Individual modules can be tested in isolation
  4. ⚡ Improved Performance: Smaller modules can be loaded on-demand
  5. 👥 Better Collaboration: Multiple developers can work on different modules simultaneously
  6. 📚 ES6 Modules: Modern import/export syntax throughout

🔒 Backward Compatibility

  • ✅ All existing function exports remain unchanged
  • ✅ No breaking changes to existing API
  • ✅ All route handlers continue to work as before
  • ✅ Test compatibility maintained

🧪 Testing

  • All existing tests continue to pass
  • New test added for optimistic locking functionality
  • No new test failures introduced

📁 File Structure

controllers/ 
├── utils.js # Utility functions 
├── crud.js # Create, Read operations 
├── delete.js # Delete operations 
├── update.js # Update operations aggregator 
├── putUpdate.js # PUT update operations 
├── patchUpdate.js # PATCH update operations 
├── patchSet.js # PATCH set operations 
├── patchUnset.js # PATCH unset operations 
├── overwrite.js # Overwrite operations 
├── bulk.js # Bulk operations 
├── history.js # History operations 
├── release.js # Release operations 
└── gog.js # Gallery of Glosses operations

🚀 Technical Implementation

  • ES6 Modules: Consistent use of modern import/export syntax
  • Clean Aggregation: Main controller cleanly imports and re-exports all functions
  • Minimal Code Changes: Function logic preserved with minimal modifications
  • Error Handling: All error handling patterns maintained
  • Performance: No performance impact, potentially improved due to better modularity

👨‍💻 Contributors

  • Patrick Cuba (@cubap) - Original implementation and architecture
  • GitHub Copilot - Assisted with refactoring and modularization
  • thehabes - Project maintenance and code review

🔍 Review Notes

This refactoring maintains 100% backward compatibility while significantly improving code organization. The changes are primarily structural - moving code from one large file into multiple smaller, focused files without altering the core business logic.


Ready for Review! 🎉

@cubap cubap requested a review from thehabes as a code owner July 3, 2025 21:36
@cubap cubap requested a review from Copilot July 3, 2025 21:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Refactors the large db-controller.js into multiple focused controller modules while maintaining existing exports and behavior.

  • Splits controllers into 13 ES6 modules (utils, crud, delete, update, putUpdate, patchUpdate, patchSet, patchUnset, overwrite, bulk, history, release, gog)
  • Updates db-controller.js to aggregate and re-export these modules
  • Adds a test for optimistic locking in overwrite

Reviewed Changes

Copilot reviewed 14 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
db-controller-backup.js Imports and re-exports all controller modules
controllers/utils.js Core utility and helper functions
controllers/crud.js Create and read operations
controllers/delete.js Soft-delete and history healing
controllers/update.js Aggregates update operations
controllers/putUpdate.js PUT-based update and import
controllers/patchUpdate.js PATCH update logic
controllers/patchSet.js PATCH set logic
controllers/patchUnset.js PATCH unset logic
controllers/overwrite.js Overwrite with optimistic locking
controllers/bulk.js Bulk create and update operations
controllers/history.js History and HEAD request handlers
controllers/release.js Release operations and tree management
controllers/gog.js Gallery of Glosses endpoints
Comments suppressed due to low confidence (1)

controllers/utils.js:73

  • The generateSlugId helper isn’t directly covered by existing tests. Adding unit tests for slug generation collision and error paths would improve confidence.
const generateSlugId = async function(slug_id="", next){

Comment thread controllers/utils.js Outdated
Comment thread controllers/gog.js Outdated
const skip = parseInt(req.query.skip ?? 0)
let err = { message: `` }
// This request can only be made my Gallery of Glosses production apps.
if (!agentID === "61043ad4ffce846a83e700dd") {
Copy link

Copilot AI Jul 3, 2025

Choose a reason for hiding this comment

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

The check !agentID === "..." always evaluates incorrectly. It should be agentID !== "61043ad4ffce846a83e700dd" to enforce the intended comparison.

Suggested change
if (!agentID === "61043ad4ffce846a83e700dd") {
if (agentID !== "61043ad4ffce846a83e700dd") {

Copilot uses AI. Check for mistakes.
Comment thread controllers/delete.js
Comment thread controllers/gog.js
Comment on lines +129 to +130
const fragmentSet = new Set(witnessFragments)
witnessFragments = Array.from(fragmentSet.values())
Copy link

Copilot AI Jul 3, 2025

Choose a reason for hiding this comment

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

Using a Set on an array of objects won't dedupe by object content but by reference. Consider deduplicating by a unique key (e.g., @id) to avoid redundant entries.

Suggested change
const fragmentSet = new Set(witnessFragments)
witnessFragments = Array.from(fragmentSet.values())
const fragmentMap = new Map()
witnessFragments.forEach(fragment => {
if (fragment['@id']) {
fragmentMap.set(fragment['@id'], fragment)
}
})
witnessFragments = Array.from(fragmentMap.values())

Copilot uses AI. Check for mistakes.
cubap and others added 6 commits July 3, 2025 16:43
where credit is due
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Comment thread controllers/crud.js
return
}
try {
let matches = await db.find(props).limit(limit).skip(skip).toArray()

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

To fix the issue, we need to ensure that the user-provided input (req.body) is sanitized or validated before being used in the database query. Specifically:

  1. Use MongoDB's $eq operator to ensure that the user input is treated as a literal value and not as a query object.
  2. Alternatively, validate that props contains only literal values (e.g., strings, numbers) and does not include any special query operators.

The best approach in this case is to validate props to ensure it contains only literal values. This ensures that the query is safe while preserving the intended functionality.


Suggested changeset 1
controllers/crud.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/controllers/crud.js b/controllers/crud.js
--- a/controllers/crud.js
+++ b/controllers/crud.js
@@ -78,2 +78,13 @@
     }
+    // Validate props to ensure it contains only literal values
+    for (const key in props) {
+        if (typeof props[key] !== "string" && typeof props[key] !== "number" && typeof props[key] !== "boolean") {
+            let err = {
+                message: `Invalid value for key '${key}'. Only string, number, or boolean values are allowed.`,
+                status: 400
+            }
+            next(createExpressError(err))
+            return
+        }
+    }
     try {
EOF
@@ -78,2 +78,13 @@
}
// Validate props to ensure it contains only literal values
for (const key in props) {
if (typeof props[key] !== "string" && typeof props[key] !== "number" && typeof props[key] !== "boolean") {
let err = {
message: `Invalid value for key '${key}'. Only string, number, or boolean values are allowed.`,
status: 400
}
next(createExpressError(err))
return
}
}
try {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread controllers/history.js
res.set("Content-Type", "application/json; charset=utf-8")
let props = req.body
try {
let matches = await db.find(props).toArray()

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

To fix the vulnerability, we need to ensure that the user-provided req.body is sanitized or validated before being used in the MongoDB query. The best approach is to enforce that props is a literal value and not a query object with special operators. This can be achieved by:

  1. Validating the structure of req.body to ensure it contains only expected fields and values.
  2. Using MongoDB's $eq operator to treat user input as literal values.

The fix involves modifying the queryHeadRequest function to validate req.body and use the $eq operator in the query. This ensures that untrusted input is interpreted as literal values, mitigating the risk of NoSQL injection.


Suggested changeset 1
controllers/history.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/controllers/history.js b/controllers/history.js
--- a/controllers/history.js
+++ b/controllers/history.js
@@ -118,4 +118,16 @@
     let props = req.body
+    if (typeof props !== 'object' || Array.isArray(props)) {
+        let err = {
+            "message": "Invalid query format. Request body must be a JSON object.",
+            "status": 400
+        }
+        next(createExpressError(err))
+        return
+    }
     try {
-        let matches = await db.find(props).toArray()
+        let sanitizedProps = {}
+        for (let key in props) {
+            sanitizedProps[key] = { $eq: props[key] }
+        }
+        let matches = await db.find(sanitizedProps).toArray()
         if (matches.length) {
EOF
@@ -118,4 +118,16 @@
let props = req.body
if (typeof props !== 'object' || Array.isArray(props)) {
let err = {
"message": "Invalid query format. Request body must be a JSON object.",
"status": 400
}
next(createExpressError(err))
return
}
try {
let matches = await db.find(props).toArray()
let sanitizedProps = {}
for (let key in props) {
sanitizedProps[key] = { $eq: props[key] }
}
let matches = await db.find(sanitizedProps).toArray()
if (matches.length) {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread controllers/utils.js
* This is the because some of the @ids have different RERUM URL patterns on them.
**/
//All the children of this object will have its @id in __rerum.history.prime
ls_versions = await db.find({ "__rerum.history.prime": rootObj['@id'] }).toArray()

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

To fix the issue, we need to ensure that the user-controlled data (rootObj['@id']) is sanitized or validated before being used in the MongoDB query. The best approach is to use the $eq operator to enforce that the input is treated as a literal value. Additionally, we can validate the input to ensure it is a string or a valid identifier before constructing the query.

Steps to fix:

  1. Modify the query in getAllVersions (line 212 of controllers/utils.js) to use the $eq operator.
  2. Add a validation step to ensure that rootObj['@id'] is a string or a valid identifier before using it in the query.

Suggested changeset 1
controllers/utils.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/controllers/utils.js b/controllers/utils.js
--- a/controllers/utils.js
+++ b/controllers/utils.js
@@ -211,3 +211,6 @@
     //All the children of this object will have its @id in __rerum.history.prime
-    ls_versions = await db.find({ "__rerum.history.prime": rootObj['@id'] }).toArray()
+    if (typeof rootObj['@id'] !== 'string') {
+        throw new Error("Invalid @id: must be a string");
+    }
+    ls_versions = await db.find({ "__rerum.history.prime": { $eq: rootObj['@id'] } }).toArray()
     //The root object is a version, prepend it in
EOF
@@ -211,3 +211,6 @@
//All the children of this object will have its @id in __rerum.history.prime
ls_versions = await db.find({ "__rerum.history.prime": rootObj['@id'] }).toArray()
if (typeof rootObj['@id'] !== 'string') {
throw new Error("Invalid @id: must be a string");
}
ls_versions = await db.find({ "__rerum.history.prime": { $eq: rootObj['@id'] } }).toArray()
//The root object is a version, prepend it in
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread controllers/utils.js
safe_descendant.__rerum.releases.previous = releasing["@id"]
let result
try {
result = await db.replaceOne({ "_id": d_id }, safe_descendant)

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

To fix the issue, we need to ensure that the _id field used in the query object { "_id": d_id } is sanitized or validated to prevent NoSQL injection. The best approach is to use MongoDB's $eq operator to ensure that the value is treated as a literal and not as a query object. Additionally, we should validate that d_id is a string or a valid ObjectID before using it in the query.

Changes will be made in the establishReleasesTree function in controllers/utils.js to:

  1. Use the $eq operator in the query object.
  2. Validate that d_id is a string or a valid ObjectID.

Suggested changeset 1
controllers/utils.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/controllers/utils.js b/controllers/utils.js
--- a/controllers/utils.js
+++ b/controllers/utils.js
@@ -311,2 +311,6 @@
         let d_id = safe_descendant._id
+        if (typeof d_id !== "string" && !ObjectID.isValid(d_id)) {
+            console.error("Invalid _id detected:", d_id)
+            return false
+        }
         safe_descendant.__rerum.releases.previous = releasing["@id"]
@@ -314,3 +318,3 @@
         try {
-            result = await db.replaceOne({ "_id": d_id }, safe_descendant)
+            result = await db.replaceOne({ "_id": { $eq: d_id } }, safe_descendant)
         } 
EOF
@@ -311,2 +311,6 @@
let d_id = safe_descendant._id
if (typeof d_id !== "string" && !ObjectID.isValid(d_id)) {
console.error("Invalid _id detected:", d_id)
return false
}
safe_descendant.__rerum.releases.previous = releasing["@id"]
@@ -314,3 +318,3 @@
try {
result = await db.replaceOne({ "_id": d_id }, safe_descendant)
result = await db.replaceOne({ "_id": { $eq: d_id } }, safe_descendant)
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread controllers/utils.js
}
let result
try {
result = await db.replaceOne({ "_id": a_id }, safe_ancestor)

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

To fix the issue, we need to ensure that the user-controlled input is sanitized or validated before being used in the MongoDB query. Specifically:

  1. Use MongoDB's $eq operator to ensure that the _id field is treated as a literal value and not as a query object.
  2. Alternatively, validate the _id field to ensure it is a valid string or ObjectID before using it in the query.

The best approach in this case is to use the $eq operator in the query object. This ensures that the input is treated as a literal value, mitigating the risk of NoSQL injection.


Suggested changeset 1
controllers/utils.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/controllers/utils.js b/controllers/utils.js
--- a/controllers/utils.js
+++ b/controllers/utils.js
@@ -314,3 +314,3 @@
         try {
-            result = await db.replaceOne({ "_id": d_id }, safe_descendant)
+            result = await db.replaceOne({ "_id": { $eq: d_id } }, safe_descendant)
         } 
@@ -334,3 +334,3 @@
         try {
-            result = await db.replaceOne({ "_id": a_id }, safe_ancestor)
+            result = await db.replaceOne({ "_id": { $eq: a_id } }, safe_ancestor)
         } 
EOF
@@ -314,3 +314,3 @@
try {
result = await db.replaceOne({ "_id": d_id }, safe_descendant)
result = await db.replaceOne({ "_id": { $eq: d_id } }, safe_descendant)
}
@@ -334,3 +334,3 @@
try {
result = await db.replaceOne({ "_id": a_id }, safe_ancestor)
result = await db.replaceOne({ "_id": { $eq: a_id } }, safe_ancestor)
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread controllers/utils.js
}
let result
try {
result = await db.replaceOne({ "_id": d_id }, safe_descendant)

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

To fix the issue, we need to ensure that the d_id value used in the query object { "_id": d_id } is properly validated or sanitized. Since MongoDB queries are susceptible to NoSQL injection, we can use the $eq operator to ensure that the _id field is treated as a literal value. This approach prevents malicious input from being interpreted as a query object.

Steps to implement the fix:

  1. Modify the query on line 384 in controllers/utils.js to use the $eq operator for the _id field.
  2. Ensure that the d_id value is validated to confirm it is a valid MongoDB ObjectID or a string, depending on the expected format.

Suggested changeset 1
controllers/utils.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/controllers/utils.js b/controllers/utils.js
--- a/controllers/utils.js
+++ b/controllers/utils.js
@@ -383,3 +383,3 @@
             try {
-                result = await db.replaceOne({ "_id": d_id }, safe_descendant)
+                result = await db.replaceOne({ "_id": { $eq: d_id } }, safe_descendant)
             } 
EOF
@@ -383,3 +383,3 @@
try {
result = await db.replaceOne({ "_id": d_id }, safe_descendant)
result = await db.replaceOne({ "_id": { $eq: d_id } }, safe_descendant)
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread controllers/utils.js
safe_ancestor.__rerum.releases.next = ancestorNextArray
let result
try {
result = await db.replaceOne({ "_id": a_id }, safe_ancestor)

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.
@cubap
Copy link
Copy Markdown
Member Author

cubap commented Jul 7, 2025

image
image
image
image
image
image

boo:
image

@cubap
Copy link
Copy Markdown
Member Author

cubap commented Jul 7, 2025

Looks like we're catching 500 instead of 409...

@cubap
Copy link
Copy Markdown
Member Author

cubap commented Jul 8, 2025

Looks like we're catching 500 instead of 409...

Great news team. I think this is actually a Tiny problem™ and not the RERUM errors:

fetch(OVERWRITE_URL, {
        method: 'PUT',
        body: JSON.stringify(obj),
        headers: {
            'Content-Type': 'application/json; charset=utf-8'
        }
    })
    .then(response => {
        if (response.ok) { return response.json() }
        throw response
    })
    .then(resultObj => {
        delete resultObj.new_obj_state
        _customEvent("rerum-result", `URI ${uri} overwritten. See resulting object below:`, resultObj)
    })
    .catch(err => {
        _customEvent("rerum-error", "There was an error trying to overwrite object at " + uri, {}, err)
    })

@cubap
Copy link
Copy Markdown
Member Author

cubap commented Jul 8, 2025

recall we changed TinyPen, but not TinyThings

@cubap
Copy link
Copy Markdown
Member Author

cubap commented Jul 8, 2025

Success:

image

Copy link
Copy Markdown
Member

@thehabes thehabes left a comment

Choose a reason for hiding this comment

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

Manually tested using TinyNode. Did a limited amount of looking through the new controllers to make sure it copied the code out correctly, did not notice any errors while using it.

@thehabes thehabes merged commit f6696dc into main Jul 8, 2025
5 of 6 checks passed
@thehabes thehabes deleted the db-controller-bustamonte branch November 6, 2025 21:07
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.

4 participants