@tiledesk/tiledesk-server 2.18.1 → 2.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,9 +5,13 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
- # 2.18.1
8
+ # 2.18.3
9
9
  - Added permissions logic
10
10
  - Added custom roles support
11
+ - Add answered questions functionality
12
+ - Added AnsweredQuestion schema with TTL index for automatic deletion.
13
+ - Updated UnansweredQuestion schema to include additional fields and improved query handling for searching and sorting.
14
+ - Enhanced the deletion process for unanswered questions.
11
15
 
12
16
  # 2.17.4
13
17
  - Refactor error handling and code structure in webhook.js
package/app.js CHANGED
@@ -130,6 +130,7 @@ var integration = require('./routes/integration')
130
130
  var kbsettings = require('./routes/kbsettings');
131
131
  var kb = require('./routes/kb');
132
132
  var unanswered = require('./routes/unanswered');
133
+ var answered = require('./routes/answered');
133
134
 
134
135
  // var admin = require('./routes/admin');
135
136
  var faqpub = require('./routes/faqpub');
@@ -643,6 +644,7 @@ app.use('/:projectid/mcp', [passport.authenticate(['basic', 'jwt'], { session: f
643
644
 
644
645
  app.use('/:projectid/kbsettings', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])], kbsettings);
645
646
  app.use('/:projectid/kb/unanswered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], unanswered);
647
+ app.use('/:projectid/kb/answered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], answered);
646
648
  app.use('/:projectid/kb', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], kb);
647
649
 
648
650
  app.use('/:projectid/logs', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('agent')], logs);
@@ -0,0 +1,153 @@
1
+ # Route `kb/answered` — domande con risposta (Knowledge Base)
2
+
3
+ Documentazione per [`routes/answered.js`](../routes/answered.js).
4
+
5
+ ## Scopo
6
+
7
+ API REST per gestire le **domande già risposte** associate a un **namespace** della Knowledge Base del progetto. I documenti sono persistiti nel modello Mongoose `AnsweredQuestion` (vedi [`models/kb_setting.js`](../models/kb_setting.js)).
8
+
9
+ Ogni operazione è vincolata al **progetto corrente** (`req.projectid`, derivato dal segmento `:projectid` nell’URL) e alla validità del **namespace** (deve esistere un `Namespace` con `id` uguale al `namespace` richiesto e `id_project` uguale al progetto).
10
+
11
+ ## Montaggio e autenticazione
12
+
13
+ In [`app.js`](../app.js) il router è montato così:
14
+
15
+ - **Path base:** `/:projectid/kb/answered`
16
+ - **Middleware:** `passport.authenticate(['basic', 'jwt'])`, `validtoken`, `roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])`
17
+
18
+ Sono quindi richiesti autenticazione (Basic o JWT), token valido e ruolo **admin** oppure tipi **bot** / **subscription**.
19
+
20
+ Esempio di URL completo (sviluppo locale):
21
+
22
+ ```http
23
+ GET http://localhost:3000/{projectId}/kb/answered/{namespace}
24
+ ```
25
+
26
+ Sostituire `{projectId}` con l’ID MongoDB del progetto.
27
+
28
+ ## Modello dati (`AnsweredQuestion`)
29
+
30
+ | Campo | Tipo | Note |
31
+ |---------------|----------|-------------------------------------------|
32
+ | `id_project` | string | Impostato dal server dal contesto progetto |
33
+ | `namespace` | string | ID del namespace (validato contro `Namespace`) |
34
+ | `question` | string | Obbligatorio |
35
+ | `answer` | string | Obbligatorio |
36
+ | `tokens` | number | Opzionale |
37
+ | `request_id` | string | Opzionale |
38
+ | `created_at` / `updated_at` | date | Da `timestamps: true` |
39
+
40
+ Indici rilevanti: TTL su `created_at`, indice composto `(id_project, namespace, created_at)`, indice **testo** su `question` e `answer` (pesi question 5, answer 1) per la ricerca full-text.
41
+
42
+ ---
43
+
44
+ ## Endpoint
45
+
46
+ Tutti i path qui sotto sono relativi a `/:projectid/kb/answered`.
47
+
48
+ ### `POST /`
49
+
50
+ Aggiunge una nuova domanda risposta.
51
+
52
+ **Body JSON**
53
+
54
+ | Campo | Obbligatorio | Descrizione |
55
+ |-------------|--------------|--------------------|
56
+ | `namespace` | sì | ID namespace |
57
+ | `question` | sì | Testo domanda |
58
+ | `answer` | sì | Testo risposta |
59
+ | `tokens` | no | Numero opzionale |
60
+ | `request_id`| no | Riferimento richiesta |
61
+
62
+ **Risposte**
63
+
64
+ | HTTP | Significato |
65
+ |------|-------------|
66
+ | 200 | Documento salvato (corpo = documento MongoDB creato) |
67
+ | 400 | Parametri mancanti (`namespace`, `question`, `answer`) |
68
+ | 403 | Namespace non appartenente al progetto |
69
+ | 500 | Errore server |
70
+
71
+ ---
72
+
73
+ ### `GET /:namespace`
74
+
75
+ Elenco paginato delle domande risposte per il namespace.
76
+
77
+ **Query**
78
+
79
+ | Parametro | Default | Descrizione |
80
+ |--------------|---------|-------------|
81
+ | `page` | `0` | Pagina (0-based) |
82
+ | `limit` | `20` | Elementi per pagina |
83
+ | `sortField` | `created_at` | Campo ordinamento |
84
+ | `direction` | `-1` | `1` crescente, `-1` decrescente |
85
+ | `search` | — | Se presente, attiva ricerca **full-text** (`$text`); l’ordinamento usa lo score di testo |
86
+
87
+ **Risposta 200**
88
+
89
+ ```json
90
+ {
91
+ "count": 0,
92
+ "questions": [],
93
+ "query": {
94
+ "page": 0,
95
+ "limit": 20,
96
+ "sortField": "created_at",
97
+ "direction": -1,
98
+ "search": "..."
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Altri codici:** `400` (namespace mancante), `403` (namespace non valido per il progetto), `500`.
104
+
105
+ ---
106
+
107
+ ### `DELETE /:id`
108
+
109
+ Elimina una singola domanda per `_id` MongoDB.
110
+
111
+ **Risposte:** `200` con `{ success, message }`, `404` se non trovata (o non del progetto), `500`.
112
+
113
+ ---
114
+
115
+ ### `DELETE /namespace/:namespace`
116
+
117
+ Elimina **tutte** le domande risposte per `namespace` nel progetto corrente.
118
+
119
+ **Risposta 200:** `{ success, count: <deletedCount>, message }`.
120
+
121
+ **403** se il namespace non appartiene al progetto. **500** in errore.
122
+
123
+ ---
124
+
125
+ ### `GET /count/:namespace`
126
+
127
+ Restituisce il conteggio documenti per `id_project` + `namespace`.
128
+
129
+ **Risposta 200:** `{ count: <number> }`.
130
+
131
+ **400** / **403** / **500** come negli altri endpoint con validazione namespace.
132
+
133
+ ---
134
+
135
+ ## Funzione interna `validateNamespace(id_project, namespace_id)`
136
+
137
+ Verifica che esista un documento `Namespace` con `id_project` e `id: namespace_id`. Usata in creazione, lettura elenco, cancellazione bulk e conteggio.
138
+
139
+ ---
140
+
141
+ ## Nota sull’ordine delle route Express
142
+
143
+ Nel file è definito `GET /:namespace` **prima** di `GET /count/:namespace`. In Express la prima route che corrisponde vince: una richiesta del tipo `GET .../kb/answered/count/<namespace>` può essere interpretata come `GET /:namespace` con `namespace` uguale alla stringa letterale `count`, invece che come endpoint di conteggio.
144
+
145
+ Per far funzionare l’URL `GET /count/:namespace` come previsto dal codice, in genere occorre registrare **`GET /count/:namespace` prima di `GET /:namespace`**, oppure usare un prefisso path diverso per uno dei due. Verificare il comportamento in ambiente reale o adeguare l’ordine delle route se il conteggio non risponde come atteso.
146
+
147
+ ---
148
+
149
+ ## Riferimenti
150
+
151
+ - Router: [`routes/answered.js`](../routes/answered.js)
152
+ - Modello e indici: [`models/kb_setting.js`](../models/kb_setting.js) (`AnsweredQuestionSchema`)
153
+ - Montaggio app: [`app.js`](../app.js) (`/:projectid/kb/answered`)
@@ -1,45 +1,104 @@
1
1
  var Project_user = require("../models/project_user");
2
2
  var winston = require('../config/winston');
3
3
 
4
+ const BATCH_SIZE = 100;
5
+ /** Log a progress line every this many bulkWrite rounds (plus always after the 1st). */
6
+ const PROGRESS_LOG_EVERY_BATCHES = 10;
7
+
8
+ /** Only documents that still need roleType (idempotent re-runs). */
9
+ const WITHOUT_ROLE_TYPE = {
10
+ $or: [{ roleType: { $exists: false } }, { roleType: null }]
11
+ };
12
+
13
+ const AGENT_ROLES_FILTER = {
14
+ $and: [
15
+ {
16
+ $or: [
17
+ { role: 'agent' },
18
+ { role: 'supervisor' },
19
+ { role: 'admin' },
20
+ { role: 'owner' }
21
+ ]
22
+ },
23
+ WITHOUT_ROLE_TYPE
24
+ ]
25
+ };
26
+
27
+ const USER_ROLES_FILTER = {
28
+ $and: [{ $or: [{ role: 'user' }, { role: 'guest' }] }, WITHOUT_ROLE_TYPE]
29
+ };
30
+
31
+ async function batchSetRoleType(filter, roleType, phaseLabel) {
32
+ const totalMatching = await Project_user.countDocuments(filter);
33
+ const started = Date.now();
34
+ winston.info(
35
+ `[project_user_role_type] ${phaseLabel}: ${totalMatching} documents match filter; streaming _id + bulkWrite in chunks of ${BATCH_SIZE}`
36
+ );
37
+
38
+ let modified = 0;
39
+ let scanned = 0;
40
+ let bulkRounds = 0;
41
+ const cursor = Project_user.find(filter).select('_id').lean().cursor();
42
+ let batch = [];
43
+
44
+ function logProgress(reason) {
45
+ const elapsedSec = ((Date.now() - started) / 1000).toFixed(1);
46
+ winston.info(
47
+ `[project_user_role_type] ${phaseLabel} ${reason}: scanned=${scanned}/${totalMatching}, modifiedThisPhase=${modified}, ${elapsedSec}s elapsed`
48
+ );
49
+ }
50
+
51
+ for await (const doc of cursor) {
52
+ scanned++;
53
+ batch.push({
54
+ updateOne: {
55
+ filter: { _id: doc._id, ...WITHOUT_ROLE_TYPE },
56
+ update: { $set: { roleType } }
57
+ }
58
+ });
59
+ if (batch.length >= BATCH_SIZE) {
60
+ const result = await Project_user.bulkWrite(batch);
61
+ modified += result.modifiedCount;
62
+ batch = [];
63
+ bulkRounds++;
64
+ if (bulkRounds === 1 || bulkRounds % PROGRESS_LOG_EVERY_BATCHES === 0) {
65
+ logProgress(`progress (bulk round ${bulkRounds})`);
66
+ }
67
+ }
68
+ }
69
+ if (batch.length > 0) {
70
+ const result = await Project_user.bulkWrite(batch);
71
+ modified += result.modifiedCount;
72
+ bulkRounds++;
73
+ logProgress(`final partial batch (bulk round ${bulkRounds})`);
74
+ }
75
+
76
+ logProgress('phase complete');
77
+ return modified;
78
+ }
79
+
4
80
  /**
5
81
  * Make any changes you need to make to the database here
6
82
  */
7
- async function up () {
8
- let p = await Project_user.find({}).exec()
9
- // Write migration here
10
- await new Promise((resolve, reject) => {
11
- Project_user.updateMany({"$or":[{ "role": "agent" }, { "role": "supervisor" }, { "role": "admin" }, { "role": "owner" }]}, {"$set": {"roleType": 1 }}, function (err, updates) {
12
- if (err) {
13
- winston.error("Error appling the migration script", err);
14
- }
15
- winston.info("Schema updated for " + updates.nModified + " project_user of type agents")
16
- winston.debug("updates",updates)
17
- // return resolve('ok');
18
- });
19
-
20
- return Project_user.updateMany({"$or":[{ "role": "user" }, { "role": "guest" }]}, {"$set": {"roleType": 2 }}, function (err, updates) {
21
- if (err) {
22
- winston.error("Error appling the migration script", err);
23
- }
24
- winston.info("Schema updated for " + updates.nModified + " project_user of type user"),
25
- winston.debug("updates",updates)
26
- return resolve('ok');
27
- });
28
-
29
-
30
- });
31
-
32
-
33
- }
34
-
35
- /**
83
+ async function up() {
84
+ try {
85
+ const agentsModified = await batchSetRoleType(AGENT_ROLES_FILTER, 1, 'agents roleType=1');
86
+ winston.info(`[project_user_role_type] Agents phase done: ${agentsModified} documents modified`);
87
+
88
+ const usersModified = await batchSetRoleType(USER_ROLES_FILTER, 2, 'users/guest roleType=2');
89
+ winston.info(`[project_user_role_type] Users/guest phase done: ${usersModified} documents modified`);
90
+ } catch (err) {
91
+ winston.error('[project_user_role_type] Error applying migration:', err);
92
+ throw err;
93
+ }
94
+ }
95
+
96
+ /**
36
97
  * Make any changes that UNDO the up function side effects here (if possible)
37
98
  */
38
- async function down () {
39
-
99
+ async function down() {
40
100
  // Write migration here
41
101
  // console.log("down*********");
42
102
  }
43
103
 
44
- module.exports = { up, down };
45
-
104
+ module.exports = { up, down };
@@ -2,10 +2,24 @@ let mongoose = require('mongoose');
2
2
  let Schema = mongoose.Schema;
3
3
  let winston = require('../config/winston');
4
4
 
5
- const expireAfterSeconds = (() => {
6
- const n = Number(process.env.UNANSWERED_QUESTION_EXPIRATION_TIME);
7
- return !isNaN(n) && n >= 0 ? n : 7 * 24 * 60 * 60; // default 7 days
8
- })();
5
+ const DEFAULT_UNANSWERED_TTL_SEC = 7 * 24 * 60 * 60; // 7 days
6
+ const DEFAULT_ANSWERED_TTL_SEC = 7 * 24 * 60 * 60; // 7 days
7
+
8
+ function ttlSecondsFromEnv(raw, fallbackSec) {
9
+ if (raw == null || String(raw).trim() === '') return fallbackSec;
10
+ const n = Number(raw);
11
+ return Number.isFinite(n) && n >= 0 ? n : fallbackSec;
12
+ }
13
+
14
+ let expireAfterSeconds = ttlSecondsFromEnv(
15
+ process.env.UNANSWERED_QUESTION_EXPIRATION_TIME,
16
+ DEFAULT_UNANSWERED_TTL_SEC
17
+ );
18
+ let expireAnsweredAfterSeconds = ttlSecondsFromEnv(
19
+ process.env.ANSWERED_QUESTION_EXPIRATION_TIME,
20
+ DEFAULT_ANSWERED_TTL_SEC
21
+ );
22
+
9
23
 
10
24
  const EngineSchema = new Schema({
11
25
  name: {
@@ -203,13 +217,63 @@ const UnansweredQuestionSchema = new Schema({
203
217
  question: {
204
218
  type: String,
205
219
  required: true
220
+ },
221
+ request_id: {
222
+ type: String,
223
+ required: false,
224
+ },
225
+ sender: {
226
+ type: String,
227
+ required: false,
206
228
  }
207
229
  },{
208
230
  timestamps: true
209
231
  });
210
232
 
211
- // Add TTL index to automatically delete documents after 30 days
212
- UnansweredQuestionSchema.index({ createdAt: 1 }, { expireAfterSeconds: expireAfterSeconds }); // 30 days
233
+ const AnsweredQuestionSchema = new Schema({
234
+ id_project: {
235
+ type: String,
236
+ required: true,
237
+ index: true
238
+ },
239
+ namespace: {
240
+ type: String,
241
+ required: true,
242
+ index: true
243
+ },
244
+ question: {
245
+ type: String,
246
+ required: true
247
+ },
248
+ answer: {
249
+ type: String,
250
+ required: true
251
+ },
252
+ tokens: {
253
+ type: Number,
254
+ required: false
255
+ },
256
+ request_id: {
257
+ type: String,
258
+ required: false
259
+ }
260
+ }, {
261
+ timestamps: true
262
+ });
263
+
264
+ // Add TTL index to automatically delete documents
265
+ UnansweredQuestionSchema.index({ createdAt: 1 }, { expireAfterSeconds: expireAfterSeconds });
266
+ UnansweredQuestionSchema.index({ id_project: 1, namespace: 1, createdAt: -1 });
267
+ UnansweredQuestionSchema.index({ question: "text" });
268
+
269
+ AnsweredQuestionSchema.index({ createdAt: 1 }, { expireAfterSeconds: expireAnsweredAfterSeconds });
270
+ AnsweredQuestionSchema.index({ id_project: 1, namespace: 1, createdAt: -1 });
271
+ AnsweredQuestionSchema.index(
272
+ { question: "text", answer: "text" },
273
+ { weights: { question: 3, answer: 1 } }
274
+ );
275
+
276
+
213
277
 
214
278
  // DEPRECATED !! - Start
215
279
  const KBSettingSchema = new Schema({
@@ -246,6 +310,7 @@ const Engine = mongoose.model('Engine', EngineSchema)
246
310
  const Namespace = mongoose.model('Namespace', NamespaceSchema)
247
311
  const KB = mongoose.model('KB', KBSchema)
248
312
  const UnansweredQuestion = mongoose.model('UnansweredQuestion', UnansweredQuestionSchema)
313
+ const AnsweredQuestion = mongoose.model('AnsweredQuestion', AnsweredQuestionSchema)
249
314
 
250
315
 
251
316
  module.exports = {
@@ -253,5 +318,6 @@ module.exports = {
253
318
  Namespace: Namespace,
254
319
  Engine: Engine,
255
320
  KB: KB,
256
- UnansweredQuestion: UnansweredQuestion
321
+ UnansweredQuestion: UnansweredQuestion,
322
+ AnsweredQuestion: AnsweredQuestion
257
323
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.18.1",
4
+ "version": "2.18.3",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -0,0 +1,227 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { Namespace, AnsweredQuestion } = require('../models/kb_setting');
4
+ const winston = require('../config/winston');
5
+
6
+ // Add a new unanswerd question
7
+ router.post('/', async (req, res) => {
8
+ try {
9
+ const { namespace, question, answer, tokens, request_id } = req.body;
10
+ const id_project = req.projectid;
11
+
12
+ if (!namespace || !question || !answer) {
13
+ return res.status(400).json({
14
+ success: false,
15
+ error: "Missing required parameters: namespace, question and answer"
16
+ })
17
+ }
18
+
19
+ // Check if namespae belongs to project
20
+ const isValidNamespace = await validateNamespace(id_project, namespace);
21
+ if (!isValidNamespace) {
22
+ return res.status(403).json({
23
+ success: false,
24
+ error: "Not allowed. The namespace does not belong to the current project."
25
+ })
26
+ }
27
+
28
+ const answeredQuestion = new AnsweredQuestion({
29
+ id_project,
30
+ namespace,
31
+ question,
32
+ answer,
33
+ tokens,
34
+ request_id,
35
+ });
36
+
37
+ const savedQuestion = await answeredQuestion.save();
38
+ res.status(200).json(savedQuestion);
39
+
40
+ } catch (error) {
41
+ winston.error('Error adding answered question:', error);
42
+ res.status(500).json({
43
+ success: false,
44
+ error: "Error adding answered question"
45
+ });
46
+ }
47
+ })
48
+
49
+ // Get all answered questions for a namespace
50
+ router.get('/:namespace', async (req, res) => {
51
+
52
+ try {
53
+ const { namespace } = req.params;
54
+ const id_project = req.projectid;
55
+
56
+ if (!namespace) {
57
+ return res.status(400).json({
58
+ success: false,
59
+ error: "Missing required parameter: namespace"
60
+ })
61
+ }
62
+
63
+ // Check if namespace belongs to project
64
+ const isValidNamespace = await validateNamespace(id_project, namespace);
65
+ if (!isValidNamespace) {
66
+ return res.status(403).json({
67
+ success: false,
68
+ error: "Not allowed. The namespace does not belong to the current project."
69
+ })
70
+ }
71
+
72
+ const page = parseInt(req.query.page) || 0;
73
+ const limit = parseInt(req.query.limit) || 20;
74
+ const sortField = req.query.sortField || 'created_at';
75
+ const direction = parseInt(req.query.direction) || -1;
76
+
77
+ const filter = { id_project, namespace };
78
+
79
+ let projection = undefined;
80
+
81
+ if (req.query.search) {
82
+ filter.$text = { $search: req.query.search };
83
+ // Add score to projection if it's a text search
84
+ projection = { score: { $meta: "textScore" } };
85
+ }
86
+
87
+ let sortObj;
88
+ if (projection && projection.score) {
89
+ sortObj = { score: { $meta: "textScore" } };
90
+ } else {
91
+ sortObj = { [sortField]: direction };
92
+ }
93
+
94
+ const questions = await AnsweredQuestion.find(filter, projection)
95
+ .sort(sortObj)
96
+ .skip(page * limit)
97
+ .limit(limit);
98
+
99
+ const count = await AnsweredQuestion.countDocuments(filter);
100
+
101
+ res.status(200).json({
102
+ count,
103
+ questions,
104
+ query: {
105
+ page,
106
+ limit,
107
+ sortField,
108
+ direction,
109
+ search: req.query.search || undefined
110
+ }
111
+ });
112
+
113
+ } catch (error) {
114
+ winston.error('Error getting answered questions:', error);
115
+ res.status(500).json({
116
+ success: false,
117
+ error: "Error getting answered questions"
118
+ });
119
+ }
120
+
121
+ })
122
+
123
+ router.delete('/:id', async (req, res) => {
124
+ try {
125
+ const { id } = req.params;
126
+ const id_project = req.projectid;
127
+
128
+ const deleted = await AnsweredQuestion.findOneAndDelete({ _id: id, id_project });
129
+ if (!deleted) {
130
+ return res.status(404).json({
131
+ success: false,
132
+ error: "Question not found"
133
+ });
134
+ }
135
+
136
+ res.status(200).json({
137
+ success: true,
138
+ message: "Question deleted successfully"
139
+ });
140
+
141
+ } catch (error) {
142
+ winston.error('Error deleting answered question:', error);
143
+ res.status(500).json({
144
+ success: false,
145
+ error: "Error deleting answered question"
146
+ });
147
+ }
148
+ })
149
+
150
+ router.delete('/namespace/:namespace', async (req, res) => {
151
+ try {
152
+ const { namespace } = req.params;
153
+ const id_project = req.projectid;
154
+
155
+ // Check if namespace belongs to project
156
+ const isValidNamespace = await validateNamespace(id_project, namespace);
157
+ if (!isValidNamespace) {
158
+ return res.status(403).json({
159
+ success: false,
160
+ error: "Not allowed. The namespace does not belong to the current project."
161
+ });
162
+ }
163
+
164
+ const result = await AnsweredQuestion.deleteMany({ id_project, namespace });
165
+ res.status(200).json({
166
+ success: true,
167
+ count: result.deletedCount,
168
+ message: "All questions deleted successfully"
169
+ });
170
+
171
+ } catch (error) {
172
+ winston.error('Error deleting answered questions:', error);
173
+ res.status(500).json({
174
+ success: false,
175
+ error: "Error deleting answered questions"
176
+ });
177
+ }
178
+ })
179
+
180
+ router.get('/count/:namespace', async (req, res) => {
181
+ try {
182
+ const { namespace } = req.params;
183
+ const id_project = req.projectid;
184
+
185
+ if (!namespace) {
186
+ return res.status(400).json({
187
+ success: false,
188
+ error: "Missing required parameter: namespace"
189
+ });
190
+ }
191
+
192
+ // Check if namespace belongs to project
193
+ const isValidNamespace = await validateNamespace(id_project, namespace);
194
+ if (!isValidNamespace) {
195
+ return res.status(403).json({
196
+ success: false,
197
+ error: "Not allowed. The namespace does not belong to the current project."
198
+ });
199
+ }
200
+
201
+ const count = await AnsweredQuestion.countDocuments({ id_project, namespace });
202
+ res.status(200).json({ count });
203
+
204
+ } catch (error) {
205
+ winston.error('Error counting answered questions:', error);
206
+ res.status(500).json({
207
+ success: false,
208
+ error: "Error counting answered questions"
209
+ });
210
+ }
211
+ })
212
+
213
+ // Helper function to validate namespace
214
+ async function validateNamespace(id_project, namespace_id) {
215
+ try {
216
+ const namespace = await Namespace.findOne({
217
+ id_project: id_project,
218
+ id: namespace_id
219
+ });
220
+ return !!namespace;
221
+ } catch (err) {
222
+ winston.error('validate namespace error: ', err);
223
+ throw err;
224
+ }
225
+ }
226
+
227
+ module.exports = router;
@@ -6,7 +6,7 @@ var winston = require('../config/winston');
6
6
  // Add a new unanswered question
7
7
  router.post('/', async (req, res) => {
8
8
  try {
9
- const { namespace, question } = req.body;
9
+ const { namespace, question, request_id, sender } = req.body;
10
10
  const id_project = req.projectid;
11
11
 
12
12
  if (!namespace || !question) {
@@ -28,7 +28,9 @@ router.post('/', async (req, res) => {
28
28
  const unansweredQuestion = new UnansweredQuestion({
29
29
  id_project,
30
30
  namespace,
31
- question
31
+ question,
32
+ request_id,
33
+ sender
32
34
  });
33
35
 
34
36
  const savedQuestion = await unansweredQuestion.save();
@@ -70,18 +72,29 @@ router.get('/:namespace', async (req, res) => {
70
72
  const sortField = req.query.sortField || 'createdAt';
71
73
  const direction = parseInt(req.query.direction) || -1;
72
74
 
73
- const questions = await UnansweredQuestion.find({
74
- id_project,
75
- namespace
76
- })
77
- .sort({ [sortField]: direction })
78
- .skip(page * limit)
79
- .limit(limit);
75
+ const filter = { id_project, namespace };
80
76
 
81
- const count = await UnansweredQuestion.countDocuments({
82
- id_project,
83
- namespace
84
- });
77
+ let projection = undefined;
78
+
79
+ if (req.query.search) {
80
+ filter.$text = { $search: req.query.search };
81
+ // Add score to projection if it's a text search
82
+ projection = { score: { $meta: "textScore" } };
83
+ }
84
+
85
+ let sortObj;
86
+ if (projection && projection.score) {
87
+ sortObj = { score: { $meta: "textScore" } };
88
+ } else {
89
+ sortObj = { [sortField]: direction };
90
+ }
91
+
92
+ const questions = await UnansweredQuestion.find(filter, projection)
93
+ .sort(sortObj)
94
+ .skip(page * limit)
95
+ .limit(limit);
96
+
97
+ const count = await UnansweredQuestion.countDocuments(filter);
85
98
 
86
99
  res.status(200).json({
87
100
  count,
@@ -90,7 +103,8 @@ router.get('/:namespace', async (req, res) => {
90
103
  page,
91
104
  limit,
92
105
  sortField,
93
- direction
106
+ direction,
107
+ search: req.query.search || undefined
94
108
  }
95
109
  });
96
110
 
@@ -109,20 +123,19 @@ router.delete('/:id', async (req, res) => {
109
123
  const { id } = req.params;
110
124
  const id_project = req.projectid;
111
125
 
112
- const question = await UnansweredQuestion.findOne({ _id: id, id_project });
113
- if (!question) {
126
+ const deleted = await UnansweredQuestion.findOneAndDelete({ _id: id, id_project });
127
+ if (!deleted) {
114
128
  return res.status(404).json({
115
129
  success: false,
116
130
  error: "Question not found"
117
131
  });
118
132
  }
119
133
 
120
- await UnansweredQuestion.deleteOne({ _id: id });
121
134
  res.status(200).json({
122
135
  success: true,
123
136
  message: "Question deleted successfully"
124
137
  });
125
-
138
+
126
139
  } catch (error) {
127
140
  winston.error('Error deleting unanswered question:', error);
128
141
  res.status(500).json({