@tiledesk/tiledesk-server 2.19.12 → 2.19.14

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/routes/request.js CHANGED
@@ -20,6 +20,7 @@ var Message = require("../models/message");
20
20
  var cacheUtil = require('../utils/cacheUtil');
21
21
  var RequestConstants = require("../models/requestConstants");
22
22
  var cacheEnabler = require("../services/cacheEnabler");
23
+ var assignmentContextUtil = require('../utils/assignmentContextUtil');
23
24
  var Project_user = require("../models/project_user");
24
25
  var Lead = require("../models/lead");
25
26
  var UIDGenerator = require("../utils/UIDGenerator");
@@ -408,7 +409,8 @@ router.post('/:requestid/participants',
408
409
  }
409
410
 
410
411
  //addParticipantByRequestId(request_id, id_project, member)
411
- return requestService.addParticipantByRequestId(req.params.requestid, req.projectid, req.body.member).then(function (updatedRequest) {
412
+ const assignmentOptions = assignmentContextUtil.buildAddParticipantOptions(req, req.body.member);
413
+ return requestService.addParticipantByRequestId(req.params.requestid, req.projectid, req.body.member, assignmentOptions).then(function (updatedRequest) {
412
414
 
413
415
  winston.verbose("participant added", updatedRequest);
414
416
 
@@ -439,7 +441,8 @@ router.put('/:requestid/participants', async (req, res) => {
439
441
  winston.debug("var participants", participants);
440
442
 
441
443
  //setParticipantsByRequestId(request_id, id_project, participants)
442
- return requestService.setParticipantsByRequestId(req.params.requestid, req.projectid, participants).then(function (updatedRequest) {
444
+ const assignmentOptions = assignmentContextUtil.buildSetParticipantsOptions(req, participants);
445
+ return requestService.setParticipantsByRequestId(req.params.requestid, req.projectid, participants, assignmentOptions).then(function (updatedRequest) {
443
446
 
444
447
  winston.debug("participant set", updatedRequest);
445
448
 
@@ -516,7 +519,8 @@ router.put('/:requestid/replace', async (req, res) => {
516
519
  participants.push(id);
517
520
  winston.verbose("participants to be set: ", participants);
518
521
 
519
- requestService.setParticipantsByRequestId(req.params.requestid, req.projectid, participants).then((updatedRequest) => {
522
+ const assignmentOptions = assignmentContextUtil.buildSetParticipantsOptions(req, participants);
523
+ requestService.setParticipantsByRequestId(req.params.requestid, req.projectid, participants, assignmentOptions).then((updatedRequest) => {
520
524
  winston.debug("SetParticipant response: ", updatedRequest);
521
525
  // Additive: include the resolved canonical (root) bot id for analytics
522
526
  // attribution. Existing consumers ignore the extra field.
@@ -531,8 +535,13 @@ router.put('/:requestid/replace', async (req, res) => {
531
535
  // TODO make a synchronous chat21 version (with query parameter?) with request.support_group.created
532
536
  router.delete('/:requestid/participants/:participantid', async (req, res) => {
533
537
  winston.debug(req.body);
534
- //removeParticipantByRequestId(request_id, id_project, member)
535
- return requestService.removeParticipantByRequestId(req.params.requestid, req.projectid, req.params.participantid).then(function (updatedRequest) {
538
+ const leaveOptions = assignmentContextUtil.buildRemoveParticipantOptions(req, req.params.participantid);
539
+ return requestService.removeParticipantByRequestId(
540
+ req.params.requestid,
541
+ req.projectid,
542
+ req.params.participantid,
543
+ leaveOptions
544
+ ).then(function (updatedRequest) {
536
545
 
537
546
  winston.verbose("participant removed", updatedRequest);
538
547
 
@@ -593,7 +602,8 @@ router.put('/:requestid/assign', function (req, res) {
593
602
  return res.json(request);
594
603
  }
595
604
  //route(request_id, departmentid, id_project) {
596
- requestService.route(req.params.requestid, req.body.departmentid, req.projectid, req.body.nobot, req.body.no_populate).then(function (updatedRequest) {
605
+ const assignmentOptions = assignmentContextUtil.buildAutoRouteOptions(req, 'api');
606
+ requestService.route(req.params.requestid, req.body.departmentid, req.projectid, req.body.nobot, req.body.no_populate, assignmentOptions).then(function (updatedRequest) {
597
607
 
598
608
  winston.debug("department changed", updatedRequest);
599
609
 
@@ -615,7 +625,8 @@ router.put('/:requestid/assign', function (req, res) {
615
625
  router.put('/:requestid/departments', function (req, res) {
616
626
  winston.debug(req.body);
617
627
  //route(request_id, departmentid, id_project) {
618
- requestService.route(req.params.requestid, req.body.departmentid, req.projectid, req.body.nobot, req.body.no_populate).then(function (updatedRequest) {
628
+ const assignmentOptions = assignmentContextUtil.buildDepartmentRouteOptions(req, 'api');
629
+ requestService.route(req.params.requestid, req.body.departmentid, req.projectid, req.body.nobot, req.body.no_populate, assignmentOptions).then(function (updatedRequest) {
619
630
 
620
631
  winston.debug("department changed", updatedRequest);
621
632
 
@@ -651,7 +662,8 @@ router.put('/:requestid/agent', async (req, res) => {
651
662
  }
652
663
  winston.debug("departmentid after: " + departmentid);
653
664
 
654
- requestService.route(req.params.requestid, departmentid, req.projectid, true, undefined).then(function (updatedRequest) {
665
+ const assignmentOptions = assignmentContextUtil.buildAutoRouteOptions(req, 'chatbot');
666
+ requestService.route(req.params.requestid, departmentid, req.projectid, true, undefined, assignmentOptions).then(function (updatedRequest) {
655
667
 
656
668
  winston.debug("department changed", updatedRequest);
657
669
 
@@ -6,11 +6,14 @@ class ChatbotService {
6
6
 
7
7
  constructor() {}
8
8
 
9
- async fork(id_faq_kb, api_url, token, project_id) {
9
+ async fork(id_faq_kb, api_url, token, project_id, options) {
10
10
  winston.debug("(ChatbotService) fork");
11
11
 
12
+ const forPublish = options && options.forPublish === true;
13
+ const forPublishQuery = forPublish ? '&for_publish=true' : '';
14
+
12
15
  return await axios({
13
- url: api_url + '/' + project_id + '/faq_kb/fork/'+id_faq_kb+"?projectid="+project_id+"&public=false&globals=true",
16
+ url: api_url + '/' + project_id + '/faq_kb/fork/' + id_faq_kb + '?projectid=' + project_id + '&public=false&globals=true' + forPublishQuery,
14
17
  headers: {
15
18
  'Content-Type': 'application/json',
16
19
  'Authorization': token
@@ -76,7 +79,7 @@ class ChatbotService {
76
79
  winston.debug("(ChatbotService) createBot");
77
80
 
78
81
  return await axios({
79
- url: api_url + '/' + project_id + '/faq_kb/',
82
+ url: api_url + '/' + project_id + '/faq_kb/?skip_activity=true',
80
83
  headers: {
81
84
  'Content-Type': 'application/json',
82
85
  'Authorization': token
@@ -0,0 +1,545 @@
1
+ const { Table, TableRow } = require('../models/dataTable');
2
+ const dataTableUtiles = require('../utils/dataTableUtils');
3
+ const { calculateBsonSize } = require('../utils/dataTableBsonUtils');
4
+ const mongoose = require('mongoose');
5
+
6
+ const DEFAULT_TABLE_MAX_SIZE_BYTES = 30 * 1024 * 1024; // 30 MB
7
+
8
+ function getTableMaxSizeBytes() {
9
+ const val = process.env.TABLE_MAX_SIZE_BYTES || DEFAULT_TABLE_MAX_SIZE_BYTES;
10
+ const n = parseInt(val, 10);
11
+ return isNaN(n) || n <= 0 ? DEFAULT_TABLE_MAX_SIZE_BYTES : n;
12
+ }
13
+
14
+ function getTableStats(table) {
15
+ if (!table) {
16
+ return { rows: 0, sizeBytes: 0 };
17
+ }
18
+ let stats;
19
+ if (typeof table.get === 'function') {
20
+ stats = table.get('stats');
21
+ } else {
22
+ stats = table.stats;
23
+ }
24
+ stats = stats || {};
25
+ return {
26
+ rows: stats.rows || 0,
27
+ sizeBytes: stats.sizeBytes || 0,
28
+ };
29
+ }
30
+
31
+ function assertTableSizeLimit(table, delta) {
32
+ if (delta <= 0) return;
33
+ const maxSize = getTableMaxSizeBytes();
34
+ const stats = getTableStats(table);
35
+ if (stats.sizeBytes + delta > maxSize) {
36
+ throw new Error('TABLE_SIZE_LIMIT_EXCEEDED');
37
+ }
38
+ }
39
+
40
+ function rowToPlainObject(row) {
41
+ return row.toObject ? row.toObject({ depopulate: true }) : row;
42
+ }
43
+
44
+ function buildInsertRowDoc(id_project, id_table, data, id_row) {
45
+ const now = new Date();
46
+ return {
47
+ _id: id_row || new mongoose.Types.ObjectId(),
48
+ id_project: id_project,
49
+ id_table: id_table,
50
+ data: data,
51
+ createdAt: now,
52
+ updatedAt: now,
53
+ };
54
+ }
55
+
56
+ function buildUpdatedRowDoc(existingRow, dataPatch) {
57
+ const plain = rowToPlainObject(existingRow);
58
+ plain.data = Object.assign({}, plain.data || {}, dataPatch);
59
+ plain.updatedAt = new Date();
60
+ return plain;
61
+ }
62
+
63
+ function dataPatchFromUpdate(update) {
64
+ const data = {};
65
+ const set = (update && update.$set) || {};
66
+ for (const key in set) {
67
+ if (Object.prototype.hasOwnProperty.call(set, key) && key.indexOf('data.') === 0) {
68
+ data[key.slice(5)] = set[key];
69
+ }
70
+ }
71
+ return data;
72
+ }
73
+
74
+ async function incrementTableStats(id_project, id_table, rowsDelta, sizeDelta) {
75
+ if (rowsDelta === 0 && sizeDelta === 0) return;
76
+ await Table.findOneAndUpdate(
77
+ { id_project: id_project, _id: id_table },
78
+ { $inc: { 'stats.rows': rowsDelta, 'stats.sizeBytes': sizeDelta } }
79
+ );
80
+ }
81
+
82
+ function computeInsertDelta(rowDoc) {
83
+ return calculateBsonSize(rowDoc);
84
+ }
85
+
86
+ function computeUpdateDelta(existingRows, update) {
87
+ const dataPatch = dataPatchFromUpdate(update);
88
+ let totalDelta = 0;
89
+ for (let i = 0; i < existingRows.length; i++) {
90
+ const oldSize = calculateBsonSize(rowToPlainObject(existingRows[i]));
91
+ const newSize = calculateBsonSize(buildUpdatedRowDoc(existingRows[i], dataPatch));
92
+ totalDelta += newSize - oldSize;
93
+ }
94
+ return totalDelta;
95
+ }
96
+
97
+ function computeDeleteDelta(existingRows) {
98
+ let totalSize = 0;
99
+ for (let i = 0; i < existingRows.length; i++) {
100
+ totalSize += calculateBsonSize(rowToPlainObject(existingRows[i]));
101
+ }
102
+ return { rowsDelta: -existingRows.length, sizeDelta: -totalSize };
103
+ }
104
+
105
+ function formatTable(table) {
106
+ const o = table.toObject ? table.toObject() : table;
107
+ return {
108
+ _id: o._id,
109
+ id_project: o.id_project,
110
+ name: o.name,
111
+ schema: o.schema || [],
112
+ stats: getTableStats(o),
113
+ createdAt: o.createdAt,
114
+ updatedAt: o.updatedAt,
115
+ };
116
+ }
117
+
118
+ function formatRow(row, schema) {
119
+ const o = row.toObject ? row.toObject() : row;
120
+ return {
121
+ _id: o._id,
122
+ id_project: o.id_project,
123
+ id_table: o.id_table,
124
+ data: dataTableUtiles.fillRowDataFromSchema(o.data, schema || []),
125
+ createdAt: o.createdAt,
126
+ updatedAt: o.updatedAt,
127
+ };
128
+ }
129
+
130
+ function formatRowForList(row, schema) {
131
+ const o = row.toObject ? row.toObject() : row;
132
+ return Object.assign(
133
+ { _id: o._id },
134
+ dataTableUtiles.fillRowDataFromSchema(o.data, schema || [])
135
+ );
136
+ }
137
+
138
+ function hasConditions(body) {
139
+ return !!(body.conditions && body.conditions.length && !body.id_row);
140
+ }
141
+
142
+ class DataTableService {
143
+
144
+ async listTables(id_project) {
145
+ const tables = await Table.find({ id_project }).sort({ createdAt: -1 });
146
+ return tables.map(formatTable);
147
+ }
148
+
149
+ async getTable(id_project, id_table) {
150
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
151
+ if (!table) return null;
152
+
153
+ const result = formatTable(table);
154
+ const rows = await TableRow.find({ id_project: id_project, id_table: id_table })
155
+ .sort({ createdAt: 1 })
156
+ .limit(100);
157
+ const schema = table.schema || [];
158
+ result.rows = rows.map(function (row) {
159
+ return dataTableUtiles.fillRowDataFromSchema(row.data, schema);
160
+ });
161
+ return result;
162
+ }
163
+
164
+ async createTable(id_project, body, createdBy) {
165
+ if (!body.name || typeof body.name !== 'string' || !body.name.trim()) {
166
+ throw new Error('name is required');
167
+ }
168
+ const schema = dataTableUtiles.normalizeSchemaForCreate(body.schema);
169
+ const table = await Table.create({
170
+ id_project: id_project,
171
+ name: body.name.trim(),
172
+ schema: schema,
173
+ createdBy: createdBy,
174
+ });
175
+ return formatTable(table);
176
+ }
177
+
178
+ async updateTable(id_project, id_table, body) {
179
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
180
+ if (!table) return null;
181
+
182
+ const update = {};
183
+ if (body.name !== undefined) {
184
+ if (!body.name || typeof body.name !== 'string' || !body.name.trim()) {
185
+ throw new Error('name is required');
186
+ }
187
+ update.name = body.name.trim();
188
+ }
189
+
190
+ if (body.schema === undefined) {
191
+ if (Object.keys(update).length === 0) {
192
+ throw new Error('name or schema is required');
193
+ }
194
+ const renamedOnly = await Table.findOneAndUpdate(
195
+ { id_project: id_project, _id: id_table },
196
+ update,
197
+ { new: true }
198
+ );
199
+ return formatTable(renamedOnly);
200
+ }
201
+
202
+ const currentSchema = table.schema || [];
203
+ const resolved = dataTableUtiles.resolveSchemaUpdate(body.schema, currentSchema);
204
+
205
+ for (let d = 0; d < resolved.deletes.length; d++) {
206
+ const delName = resolved.deletes[d];
207
+ await TableRow.updateMany(
208
+ { id_project: id_project, id_table: id_table },
209
+ { $unset: { ['data.' + delName]: '' } }
210
+ );
211
+ }
212
+
213
+ for (let r = 0; r < resolved.renames.length; r++) {
214
+ const ren = resolved.renames[r];
215
+ await TableRow.updateMany(
216
+ { id_project: id_project, id_table: id_table },
217
+ { $rename: { ['data.' + ren.oldName]: 'data.' + ren.newName } }
218
+ );
219
+ }
220
+
221
+ update.schema = resolved.schema;
222
+ const updated = await Table.findOneAndUpdate(
223
+ { id_project: id_project, _id: id_table },
224
+ update,
225
+ { new: true }
226
+ );
227
+ return formatTable(updated);
228
+ }
229
+
230
+ async deleteTable(id_project, id_table) {
231
+ const table = await Table.findOneAndDelete({ id_project: id_project, _id: id_table });
232
+ if (!table) return false;
233
+ await TableRow.deleteMany({ id_project: id_project, id_table: id_table });
234
+ return true;
235
+ }
236
+
237
+ async addColumn(id_project, id_table, columnInput) {
238
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
239
+ if (!table) return null;
240
+
241
+ const schema = table.schema || [];
242
+ const newCol = dataTableUtiles.normalizeColumnToAdd(columnInput, schema.length, schema);
243
+ schema.push(newCol);
244
+
245
+ const updated = await Table.findOneAndUpdate(
246
+ { id_project: id_project, _id: id_table },
247
+ { schema: schema },
248
+ { new: true }
249
+ );
250
+ return formatTable(updated);
251
+ }
252
+
253
+ async renameColumn(id_project, id_table, columnId, newName) {
254
+ if (!newName || typeof newName !== 'string' || !newName.trim()) {
255
+ throw new Error('name is required');
256
+ }
257
+ const trimmed = newName.trim();
258
+
259
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
260
+ if (!table) return null;
261
+
262
+ const schema = table.schema || [];
263
+ const col = dataTableUtiles.findColumn(schema, columnId);
264
+ if (!col) throw new Error('Column not found');
265
+
266
+ if (col.name === trimmed) return formatTable(table);
267
+
268
+ if (dataTableUtiles.findColumnByName(schema, trimmed)) {
269
+ throw new Error('Column name already exists: ' + trimmed);
270
+ }
271
+
272
+ const oldName = col.name;
273
+ const newSchema = schema.map(function (c) {
274
+ if (c.id === columnId) return { id: c.id, name: trimmed, type: c.type, index: c.index };
275
+ return c;
276
+ });
277
+
278
+ await TableRow.updateMany(
279
+ { id_project: id_project, id_table: id_table },
280
+ { $rename: { ['data.' + oldName]: 'data.' + trimmed } }
281
+ );
282
+
283
+ const updated = await Table.findOneAndUpdate(
284
+ { id_project: id_project, _id: id_table },
285
+ { schema: newSchema },
286
+ { new: true }
287
+ );
288
+ return formatTable(updated);
289
+ }
290
+
291
+ async deleteColumn(id_project, id_table, columnId) {
292
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
293
+ if (!table) return null;
294
+
295
+ const schema = table.schema || [];
296
+ const col = dataTableUtiles.findColumn(schema, columnId);
297
+ if (!col) throw new Error('Column not found');
298
+
299
+ const newSchema = schema.filter(function (c) { return c.id !== columnId; });
300
+
301
+ await TableRow.updateMany(
302
+ { id_project: id_project, id_table: id_table },
303
+ { $unset: { ['data.' + col.name]: '' } }
304
+ );
305
+
306
+ const updated = await Table.findOneAndUpdate(
307
+ { id_project: id_project, _id: id_table },
308
+ { schema: newSchema },
309
+ { new: true }
310
+ );
311
+ return formatTable(updated);
312
+ }
313
+
314
+ async listRows(id_project, id_table) {
315
+ return this.listRowsWithFilter(id_project, id_table, {});
316
+ }
317
+
318
+ async listRowsWithFilter(id_project, id_table, params) {
319
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
320
+ if (!table) return null;
321
+
322
+ const schema = table.schema || [];
323
+ let conditions = params.conditions;
324
+
325
+ if (typeof conditions === 'string') {
326
+ try {
327
+ conditions = JSON.parse(conditions);
328
+ } catch (e) {
329
+ throw new Error('conditions must be a valid JSON array');
330
+ }
331
+ }
332
+
333
+ const baseQuery = { id_project: id_project, id_table: id_table };
334
+
335
+ if (conditions && Array.isArray(conditions) && conditions.length > 0) {
336
+ const filter = dataTableUtiles.buildSearchFilter({
337
+ must_match: params.must_match,
338
+ match: params.match,
339
+ conditions: conditions,
340
+ }, schema);
341
+ Object.assign(baseQuery, filter);
342
+ }
343
+
344
+ const sortOrder = params.sort === -1 || params.sort === 'desc' ? -1 : 1;
345
+ let query = TableRow.find(baseQuery).sort({ createdAt: sortOrder });
346
+ if (params.limit != null) {
347
+ const limit = parseInt(params.limit, 10);
348
+ if (!isNaN(limit) && limit > 0) {
349
+ query = query.limit(limit);
350
+ }
351
+ }
352
+ const rows = await query;
353
+ return rows.map(function (row) {
354
+ return formatRowForList(row, schema);
355
+ });
356
+ }
357
+
358
+ async getRow(id_project, id_table, id_row) {
359
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
360
+ if (!table) return null;
361
+ const row = await TableRow.findOne({ id_project: id_project, id_table: id_table, _id: id_row });
362
+ if (!row) return null;
363
+ return formatRow(row, table.schema || []);
364
+ }
365
+
366
+ async insertRow(id_project, id_table, rawData) {
367
+ return this.insertRowByBody(id_project, id_table, { data: rawData });
368
+ }
369
+
370
+ async insertRowByBody(id_project, id_table, body) {
371
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
372
+ if (!table) return null;
373
+
374
+ if (body.data === undefined || body.data === null) {
375
+ throw new Error('data is required');
376
+ }
377
+ const data = dataTableUtiles.validateRowData(body.data, table.schema || []);
378
+ const rowDoc = buildInsertRowDoc(id_project, id_table, data, body.id_row);
379
+ const rowSize = computeInsertDelta(rowDoc);
380
+ assertTableSizeLimit(table, rowSize);
381
+
382
+ const row = await TableRow.create({
383
+ _id: rowDoc._id,
384
+ id_project: id_project,
385
+ id_table: id_table,
386
+ data: data,
387
+ });
388
+ await incrementTableStats(id_project, id_table, 1, rowSize);
389
+ return formatRow(row, table.schema || []);
390
+ }
391
+
392
+ async updateRow(id_project, id_table, id_row, rawData) {
393
+ return this.updateRowByBody(id_project, id_table, { id_row: id_row, data: rawData });
394
+ }
395
+
396
+ async updateRowByBody(id_project, id_table, body) {
397
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
398
+ if (!table) return null;
399
+
400
+ const schema = table.schema || [];
401
+ const update = dataTableUtiles.buildUpdateSet(body.data, schema);
402
+ const rowFilter = dataTableUtiles.buildRowQueryFilter(body, schema);
403
+ const filter = { id_project: id_project, id_table: id_table };
404
+ Object.assign(filter, rowFilter);
405
+
406
+ if (hasConditions(body)) {
407
+ const matches = await TableRow.find(filter).sort({ createdAt: 1 });
408
+ if (matches.length === 0) return undefined;
409
+ const sizeDelta = computeUpdateDelta(matches, update);
410
+ assertTableSizeLimit(table, sizeDelta);
411
+ await TableRow.updateMany(filter, update);
412
+ await incrementTableStats(id_project, id_table, 0, sizeDelta);
413
+ const updatedRows = await TableRow.find(filter).sort({ createdAt: 1 });
414
+ return updatedRows.map(function (r) { return formatRow(r, schema); });
415
+ }
416
+
417
+ const existing = await TableRow.findOne(filter);
418
+ if (!existing) return undefined;
419
+ const singleDelta = computeUpdateDelta([existing], update);
420
+ assertTableSizeLimit(table, singleDelta);
421
+ const row = await TableRow.findOneAndUpdate(filter, update, { new: true });
422
+ await incrementTableStats(id_project, id_table, 0, singleDelta);
423
+ return formatRow(row, schema);
424
+ }
425
+
426
+ async upsertRowByBody(id_project, id_table, body) {
427
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
428
+ if (!table) return null;
429
+
430
+ const schema = table.schema || [];
431
+ if (!body.data || typeof body.data !== 'object' || Array.isArray(body.data)) {
432
+ throw new Error('data is required');
433
+ }
434
+
435
+ const update = dataTableUtiles.buildUpdateSet(body.data, schema);
436
+ const rowData = dataTableUtiles.validateRowData(body.data, schema);
437
+
438
+ if (body.id_row) {
439
+ const filterById = { id_project: id_project, id_table: id_table, _id: body.id_row };
440
+ const existing = await TableRow.findOne(filterById);
441
+ if (existing) {
442
+ const updateDelta = computeUpdateDelta([existing], update);
443
+ assertTableSizeLimit(table, updateDelta);
444
+ const updated = await TableRow.findOneAndUpdate(filterById, update, { new: true });
445
+ await incrementTableStats(id_project, id_table, 0, updateDelta);
446
+ return formatRow(updated, schema);
447
+ }
448
+ const insertDoc = buildInsertRowDoc(id_project, id_table, rowData, body.id_row);
449
+ const insertSize = computeInsertDelta(insertDoc);
450
+ assertTableSizeLimit(table, insertSize);
451
+ try {
452
+ const created = await TableRow.create({
453
+ _id: body.id_row,
454
+ id_project: id_project,
455
+ id_table: id_table,
456
+ data: rowData,
457
+ });
458
+ await incrementTableStats(id_project, id_table, 1, insertSize);
459
+ return formatRow(created, schema);
460
+ } catch (createErr) {
461
+ if (createErr.code === 11000) {
462
+ const racedExisting = await TableRow.findOne(filterById);
463
+ if (racedExisting) {
464
+ const racedDelta = computeUpdateDelta([racedExisting], update);
465
+ assertTableSizeLimit(table, racedDelta);
466
+ const raced = await TableRow.findOneAndUpdate(filterById, update, { new: true });
467
+ await incrementTableStats(id_project, id_table, 0, racedDelta);
468
+ if (raced) return formatRow(raced, schema);
469
+ }
470
+ }
471
+ throw createErr;
472
+ }
473
+ }
474
+
475
+ const rowFilter = dataTableUtiles.buildRowQueryFilter(body, schema);
476
+ const filter = { id_project: id_project, id_table: id_table };
477
+ Object.assign(filter, rowFilter);
478
+ const matches = await TableRow.find(filter).sort({ createdAt: 1 });
479
+
480
+ if (matches.length === 0) {
481
+ const newDoc = buildInsertRowDoc(id_project, id_table, rowData);
482
+ const newSize = computeInsertDelta(newDoc);
483
+ assertTableSizeLimit(table, newSize);
484
+ const inserted = await TableRow.create({ id_project: id_project, id_table: id_table, data: rowData });
485
+ await incrementTableStats(id_project, id_table, 1, newSize);
486
+ return formatRow(inserted, schema);
487
+ }
488
+
489
+ if (matches.length > 1) {
490
+ throw new Error('Multiple rows match the conditions');
491
+ }
492
+
493
+ const upsertDelta = computeUpdateDelta(matches, update);
494
+ assertTableSizeLimit(table, upsertDelta);
495
+ const updated = await TableRow.findOneAndUpdate(filter, update, { new: true });
496
+ await incrementTableStats(id_project, id_table, 0, upsertDelta);
497
+ return formatRow(updated, schema);
498
+ }
499
+
500
+ async deleteRow(id_project, id_table, id_row) {
501
+ return this.deleteRowByBody(id_project, id_table, { id_row: id_row });
502
+ }
503
+
504
+ async deleteRowByBody(id_project, id_table, body) {
505
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
506
+ if (!table) return null;
507
+
508
+ const rowFilter = dataTableUtiles.buildRowQueryFilter(body, table.schema || []);
509
+ const filter = { id_project: id_project, id_table: id_table };
510
+ Object.assign(filter, rowFilter);
511
+
512
+ const schema = table.schema || [];
513
+
514
+ if (hasConditions(body)) {
515
+ const rows = await TableRow.find(filter).sort({ createdAt: 1 });
516
+ if (rows.length === 0) return undefined;
517
+ const formatted = rows.map(function (r) { return formatRow(r, schema); });
518
+ const deleteDelta = computeDeleteDelta(rows);
519
+ await TableRow.deleteMany(filter);
520
+ await incrementTableStats(id_project, id_table, deleteDelta.rowsDelta, deleteDelta.sizeDelta);
521
+ return formatted;
522
+ }
523
+
524
+ const row = await TableRow.findOne(filter);
525
+ if (!row) return undefined;
526
+ const singleDeleteDelta = computeDeleteDelta([row]);
527
+ const deleted = await TableRow.findOneAndDelete(filter);
528
+ await incrementTableStats(id_project, id_table, singleDeleteDelta.rowsDelta, singleDeleteDelta.sizeDelta);
529
+ return formatRow(deleted, schema);
530
+ }
531
+
532
+ async searchRows(id_project, id_table, searchBody) {
533
+ const table = await Table.findOne({ id_project: id_project, _id: id_table });
534
+ if (!table) return null;
535
+
536
+ const schema = table.schema || [];
537
+ const filter = dataTableUtiles.buildSearchFilter(searchBody, schema);
538
+ const query = { id_project: id_project, id_table: id_table };
539
+ Object.assign(query, filter);
540
+ const rows = await TableRow.find(query);
541
+ return rows.map(function (r) { return formatRow(r, schema); });
542
+ }
543
+ }
544
+
545
+ module.exports = new DataTableService();