From bc6bd06a15c4cbeb4205460734fc491e9e642f7b Mon Sep 17 00:00:00 2001 From: Stefan Seifert Date: Mon, 19 Aug 2024 12:12:35 +0200 Subject: [PATCH] Ensure unique question numbers for Q&A entries (#15) --- package-lock.json | 11 +++++++++++ package.json | 1 + src/repository/mongodb.schema.ts | 1 + src/socket/talkRoomQAEntries.ts | 29 +++++++++++++++++++++++------ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index e8f08d6..394db04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.10.3", "@types/express": "^4.17.21", + "@types/mongodb": "^4.0.7", "@types/uuid": "^10.0.0", "eslint": "^8.57.0", "eslint-config-typescript": "^3.0.0", @@ -606,6 +607,16 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/mongodb": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-4.0.7.tgz", + "integrity": "sha512-lPUYPpzA43baXqnd36cZ9xxorprybxXDzteVKCPAdp14ppHtFJHnXYvNpmBvtMUTb5fKXVv6sVbzo1LHkWhJlw==", + "deprecated": "mongodb provides its own types. @types/mongodb is no longer needed.", + "dev": true, + "dependencies": { + "mongodb": "*" + } + }, "node_modules/@types/node": { "version": "20.14.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", diff --git a/package.json b/package.json index a34ff00..6dfa47b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.10.3", "@types/express": "^4.17.21", + "@types/mongodb": "^4.0.7", "@types/uuid": "^10.0.0", "eslint": "^8.57.0", "eslint-config-typescript": "^3.0.0", diff --git a/src/repository/mongodb.schema.ts b/src/repository/mongodb.schema.ts index 4d3828c..5a0cd09 100644 --- a/src/repository/mongodb.schema.ts +++ b/src/repository/mongodb.schema.ts @@ -124,6 +124,7 @@ const QAEntrySchema = new mongoose.Schema({ answered: { type: Boolean, required:false } }) QAEntrySchema.index({userid: 1, username: 1}) +QAEntrySchema.index({talkId: 1, entryIndex: 1}, { unique: true, partialFilterExpression: { entryIndex: { $gt: 0 } } }) export const QAEntryModel = mongoose.model('qa-entry', QAEntrySchema) export interface QAEntryLike { diff --git a/src/socket/talkRoomQAEntries.ts b/src/socket/talkRoomQAEntries.ts index 6141963..0d9afad 100644 --- a/src/socket/talkRoomQAEntries.ts +++ b/src/socket/talkRoomQAEntries.ts @@ -6,6 +6,7 @@ import log from '../util/log' import { qaEntryAnsweredToServerObject, qaEntryLikeToServerObject, qaEntryToServerObject, uuidString } from '../repository/validation.schema' import isInputValid from '../util/isInputValid' import { v4 as uuidv4 } from 'uuid' +import { MongoError } from 'mongodb' export async function handleTalkRoomQAEntries(socket : Socket) { const { userid, username, admin, qaadmin } = socket.data @@ -20,25 +21,41 @@ export async function handleTalkRoomQAEntries(socket : Socket void) { + async function handleNew(newQaEntry: QAEntryToServer, callback: (result: OperationResult, entryIndex?: number) => void, retryCount: number = 1) { if (!isInputValid(qaEntryToServerObject, newQaEntry, callback)) { return } - const { id, talkId, text, anonymous, replyTo, highlight, answered } = newQaEntry - log.debug(`User ${username} created Q&A entry in ${talkId}: ${text}`) const date = new Date() const qaEntryUsername = anonymous ? undefined : username + if (retryCount > 100) { + log.error(`User ${username} tried to create Q&A entry in ${talkId}, unable to find unique entryIndex after 100 retries.`) + callback({success: false, error: 'Error creating Q&A entry: Unable to find unique entryIndex.'}) + return + } + let entryIndex = 0 if (!replyTo) { const maxEntryIndex = (await QAEntryModel.findOne({talkId}).sort({entryIndex:-1}).exec())?.entryIndex ?? 0 entryIndex = maxEntryIndex + 1 } - await QAEntryModel.create({ _id:id, talkId, date, userid, username: qaEntryUsername, text, entryIndex, replyTo, highlight, answered }) - callback({success: true}, entryIndex) - socket.in(talkId).emit('qaEntries', [{id, date, userid, username: qaEntryUsername, text, entryIndex, replyTo, highlight, answered, likeUserIds: []}]) + try { + await QAEntryModel.create({ _id:id, talkId, date, userid, username: qaEntryUsername, text, entryIndex, replyTo, highlight, answered }) + log.debug(`User ${username} created Q&A entry in ${talkId}: ${text}`) + callback({success: true}, entryIndex) + socket.in(talkId).emit('qaEntries', [{id, date, userid, username: qaEntryUsername, text, entryIndex, replyTo, highlight, answered, likeUserIds: []}]) + } + catch (error) { + if ((error instanceof MongoError) && error.code === 11000) { + log.debug(`User ${username} tried to create Q&A entry in ${talkId}, but entryIndex is a duplicate: ${entryIndex}; try again...`) + handleNew(newQaEntry, callback, retryCount + 1) + return + } + log.error(`User ${username} tried to create Q&A entry in ${talkId}, resulted in error: ${error}`) + callback({success: false, error: `Error creating Q&A entry: ${error}`}) + } } /**