From 23711c934ae122d3a93d93abe46ccdf454504d74 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:21:31 -0600 Subject: [PATCH 1/7] Fix async/await patterns in Group class methods (#439) * Initial plan * Fix async/await patterns in Group.js - return promises instead of just awaiting Co-authored-by: cubap <1119165+cubap@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cubap <1119165+cubap@users.noreply.github.com> --- classes/Group/Group.js | 14 +++++++------- package-lock.json | 6 +----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/classes/Group/Group.js b/classes/Group/Group.js index 5098bfcc..ddef282e 100644 --- a/classes/Group/Group.js +++ b/classes/Group/Group.js @@ -92,7 +92,7 @@ export default class Group { roles = washRoles(roles) this.data.members[memberId].roles = roles - await this.update() + return this.update() } /** @@ -158,7 +158,7 @@ export default class Group { } this.data.members[memberId].roles = this.data.members[memberId].roles.filter(role => !roles.includes(role)) - await this.update() + return this.update() } async removeMember(memberId) { @@ -194,7 +194,7 @@ export default class Group { if (!this.isValidRolesMap(roles)) throw new Error("Invalid roles. Must be a JSON Object with keys as roles and values as arrays of permissions or space-delimited strings.") this.data.customRoles = roles - await this.update() + return this.update() } async addCustomRoles(roleMap) { @@ -204,7 +204,7 @@ export default class Group { if (!this.isValidRolesMap(roleMap)) throw new Error("Invalid roles. Must be a JSON Object with keys as roles and values as arrays of permissions or space-delimited strings.") this.data.customRoles = { ...this.data.customRoles, ...roleMap } - await this.update() + return this.update() } async removeCustomRoles(roleName) { @@ -213,7 +213,7 @@ export default class Group { } delete this.data.customRoles[roleName] - await this.update() + return this.update() } async save() { @@ -248,10 +248,10 @@ export default class Group { } if (!this.getByRole("OWNER")?.length) { - await this.addMemberRoles(this.data.creator, "OWNER", true) + this.addMemberRoles(this.data.creator, "OWNER", true) } if (!this.getByRole("LEADER")?.length) { - await this.addMemberRoles(this.data.creator, "LEADER") + this.addMemberRoles(this.data.creator, "LEADER") } } diff --git a/package-lock.json b/package-lock.json index a717668e..dc69cbca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,7 +120,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -670,7 +669,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -715,7 +713,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -801,7 +798,6 @@ "resolved": "https://registry.npmjs.org/@iiif/presentation-3/-/presentation-3-2.2.3.tgz", "integrity": "sha512-xCLbUr9euqegsrxGe65M2fWbv6gKpiUhHXCpOn+V+qtawkMbOSNWbYOISo2aLQdYVg4DGYD0g2bMzSCF33uNOQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/geojson": "^7946.0.10" } @@ -2120,7 +2116,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -4899,6 +4894,7 @@ } ], "license": "MIT", + "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, From 2a7aeb0b704ac2d32ee2e940665a54868d955af4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:01:31 +0000 Subject: [PATCH 2/7] Initial plan From 95240b17b38efa6883a3a474d8580649eb46d012 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:04:28 +0000 Subject: [PATCH 3/7] Update TinyPEN Content-Type to application/ld+json Co-authored-by: cubap <1119165+cubap@users.noreply.github.com> --- database/tiny/controller.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/database/tiny/controller.js b/database/tiny/controller.js index 09c9313b..2b9b4d00 100644 --- a/database/tiny/controller.js +++ b/database/tiny/controller.js @@ -98,7 +98,7 @@ class DatabaseController { method: 'post', body: JSON.stringify(query), headers: { - 'Content-Type': 'application/json; charset=utf-8' + 'Content-Type': 'application/ld+json; charset=utf-8' } }) .then(resp => { @@ -129,7 +129,7 @@ class DatabaseController { method: 'post', body: JSON.stringify(data), headers: { - 'Content-Type': 'application/json; charset=utf-8' + 'Content-Type': 'application/ld+json; charset=utf-8' } }) .then(resp => { @@ -160,7 +160,7 @@ class DatabaseController { method: 'put', body: JSON.stringify(data), headers: { - 'Content-Type': 'application/json; charset=utf-8' + 'Content-Type': 'application/ld+json; charset=utf-8' } }) .then(resp => { @@ -190,7 +190,7 @@ class DatabaseController { err_out._dbaction = this.URLS.OVERWRITE const headers = { - 'Content-Type': 'application/json; charset=utf-8' + 'Content-Type': 'application/ld+json; charset=utf-8' } // Add optimistic locking header if __rerum.isOverwritten exists @@ -255,7 +255,7 @@ class DatabaseController { method: 'delete', body: JSON.stringify(data), headers: { - 'Content-Type': 'application/json; charset=utf-8' + 'Content-Type': 'application/ld+json; charset=utf-8' } }) .then(resp => { From c92e4f8de979dcaeab7194818ce2827879edf513 Mon Sep 17 00:00:00 2001 From: cubap Date: Mon, 23 Feb 2026 16:08:35 -0600 Subject: [PATCH 4/7] caller choses whether to save after --- classes/Group/Group.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/classes/Group/Group.js b/classes/Group/Group.js index 9c7a3b36..715f7ece 100644 --- a/classes/Group/Group.js +++ b/classes/Group/Group.js @@ -99,8 +99,10 @@ export default class Group { * Add if not in roles for a member with the provided roles. * @param {String} memberId _id of the member * @param {Array | String} roles [ROLE, ROLE, ...] or "ROLE ROLE ..." + * @param {boolean} allowOwner Allow assignment of OWNER role + * @param {boolean} shouldUpdate Persist changes when true */ - async addMemberRoles(memberId, roles, allowOwner = false) { + async addMemberRoles(memberId, roles, allowOwner = false, shouldUpdate = true) { if (!Object.keys(this.data.members).length) { await this.#loadFromDB() @@ -122,8 +124,9 @@ export default class Group { } roles = washRoles(roles, allowOwner) this.data.members[memberId].roles = [...new Set([...this.data.members[memberId].roles, ...roles])] - // If we need the group to update first (caused error) - // await this.update() + if (shouldUpdate) { + await this.update() + } } /** @@ -172,8 +175,9 @@ export default class Group { * * @param {string} memberId The User/member _id to remove from the Group and perhaps delete from the db. * @param {boolean} voluntary Whether the user is leaving voluntarily (true) or being removed by admin (false). + * @param {boolean} shouldUpdate Persist changes when true */ - async removeMember(memberId, voluntary = false) { + async removeMember(memberId, voluntary = false, shouldUpdate = true) { if (!Object.keys(this.data.members).length) { await this.#loadFromDB() } @@ -196,8 +200,9 @@ export default class Group { } } delete this.data.members[memberId] - // If we need the Group to update in the db in addition to the Class data. - // await this.update() + if (shouldUpdate) { + await this.update() + } } /** @@ -309,11 +314,11 @@ export default class Group { } if (!this.getByRole("OWNER")?.length) { - await this.addMemberRoles(this.data.creator, "OWNER", true) + await this.addMemberRoles(this.data.creator, "OWNER", true, false) } if (!this.getByRole("LEADER")?.length) { - await this.addMemberRoles(this.data.creator, "LEADER") - } + await this.addMemberRoles(this.data.creator, "LEADER", false, false) + } } static async createNewGroup(creator, payload) { From ef0832c5d3b6d328b3989584a2903975b83b8f3f Mon Sep 17 00:00:00 2001 From: cubap Date: Mon, 23 Feb 2026 16:14:40 -0600 Subject: [PATCH 5/7] Add shouldUpdate flag to Group role methods Introduce an optional shouldUpdate parameter to Group.setMemberRoles and Group.removeMemberRoles to control whether changes are immediately persisted. These methods now skip calling update() when shouldUpdate is false, allowing callers to batch multiple role mutations and perform a single update. Call sites updated: removed a redundant update in Project.removeMember flow and adjusted memberRouter to pass shouldUpdate=false for intermediate role changes, then call group.update() once after all changes. This reduces redundant DB writes and improves efficiency. --- classes/Group/Group.js | 15 +++++++++++---- classes/Project/Project.js | 1 - project/memberRouter.js | 7 +++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/classes/Group/Group.js b/classes/Group/Group.js index 715f7ece..2f491e63 100644 --- a/classes/Group/Group.js +++ b/classes/Group/Group.js @@ -68,8 +68,9 @@ export default class Group { * Replace all roles for a member with the provided roles. * @param {String} memberId _id of the member * @param {Array | String} roles [ROLE, ROLE, ...] or "ROLE ROLE ..." + * @param {boolean} shouldUpdate Persist changes when true */ - async setMemberRoles(memberId, roles) { + async setMemberRoles(memberId, roles, shouldUpdate = true) { if (!Object.keys(this.data.members).length) { await this.#loadFromDB() } @@ -92,7 +93,9 @@ export default class Group { roles = washRoles(roles) this.data.members[memberId].roles = roles - await this.update() + if (shouldUpdate) { + await this.update() + } } /** @@ -133,8 +136,10 @@ export default class Group { * Remove roles if found for a member. * @param {String} memberId _id of the member * @param {Array | String} roles [ROLE, ROLE, ...] or "ROLE ROLE ..." + * @param {boolean} allowOwner Allow removal of OWNER role + * @param {boolean} shouldUpdate Persist changes when true */ - async removeMemberRoles(memberId, roles, allowOwner = false) { + async removeMemberRoles(memberId, roles, allowOwner = false, shouldUpdate = true) { if (!Object.keys(this.data.members).length) { await this.#loadFromDB() } @@ -163,7 +168,9 @@ export default class Group { } this.data.members[memberId].roles = this.data.members[memberId].roles.filter(role => !roles.includes(role)) - await this.update() + if (shouldUpdate) { + await this.update() + } } /** diff --git a/classes/Project/Project.js b/classes/Project/Project.js index 2eafcb23..6e6f6ed3 100644 --- a/classes/Project/Project.js +++ b/classes/Project/Project.js @@ -235,7 +235,6 @@ export default class Project { const user = new User(userId) const userData = await user.getSelf() await group.removeMember(userId, voluntary) - await group.update() const projectTitle = this.data?.label ?? this.data?.title ?? 'TPEN Project' try { // Send confirmation email (non-blocking) diff --git a/project/memberRouter.js b/project/memberRouter.js index 8be9ae65..1e21f581 100644 --- a/project/memberRouter.js +++ b/project/memberRouter.js @@ -72,7 +72,6 @@ router.route("/:projectId/collaborator/:collaboratorId/addRoles").post(auth0Midd const groupId = projectObj.data.group const group = new Group(groupId) await group.addMemberRoles(collaboratorId, roles) - await group.update() res.status(200).send(`Roles added to member ${collaboratorId}.`) } catch (error) { return respondWithError(res, error.status ?? 500, error.message ?? "Error adding roles to member.") @@ -137,9 +136,9 @@ router.route("/:projectId/switch/owner").post(auth0Middleware(), async (req, res } const group = new Group(projectObj.data.group) const currentRoles = await group.getMemberRoles(user._id) - Object.keys(currentRoles).length === 1 && await group.addMemberRoles(user._id, ["CONTRIBUTOR"]) - await group.addMemberRoles(newOwnerId, ["OWNER"], true) - await group.removeMemberRoles(user._id, ["OWNER"], true) + Object.keys(currentRoles).length === 1 && await group.addMemberRoles(user._id, ["CONTRIBUTOR"], false, false) + await group.addMemberRoles(newOwnerId, ["OWNER"], true, false) + await group.removeMemberRoles(user._id, ["OWNER"], true, false) await group.update() res.status(200).json({ message: `Ownership successfully transferred to member ${newOwnerId}.` }) } catch (error) { From 54d71052c156fbdae996a3041c5cf134202b785c Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 26 Feb 2026 11:55:25 -0600 Subject: [PATCH 6/7] auditfix --- package-lock.json | 58 ++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index a405827c..5cb363f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2092,13 +2092,24 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, - "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2703,11 +2714,10 @@ } }, "node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -4737,13 +4747,12 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", + "integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5010,11 +5019,10 @@ } }, "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5429,10 +5437,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dependencies": { "side-channel": "^1.1.0" }, @@ -6209,11 +6216,10 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, From ff7dd53837239090763123bfe253385cc47c01f1 Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Thu, 26 Feb 2026 12:04:15 -0600 Subject: [PATCH 7/7] Update classes/Group/Group.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- classes/Group/Group.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/Group/Group.js b/classes/Group/Group.js index 2f491e63..ed1685b9 100644 --- a/classes/Group/Group.js +++ b/classes/Group/Group.js @@ -324,8 +324,8 @@ export default class Group { await this.addMemberRoles(this.data.creator, "OWNER", true, false) } if (!this.getByRole("LEADER")?.length) { - await this.addMemberRoles(this.data.creator, "LEADER", false, false) - } + await this.addMemberRoles(this.data.creator, "LEADER", false, false) + } } static async createNewGroup(creator, payload) {