@tiledesk/tiledesk-server 2.19.11 → 2.19.13
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 +7 -0
- package/app.js +3 -0
- package/models/dataTable.js +64 -0
- package/package.json +3 -3
- package/routes/dataTable.js +280 -0
- package/services/dataTableService.js +545 -0
- package/services/requestService.js +21 -16
- package/test/dataTablesRoute.test.js +844 -0
- package/utils/dataTableBsonUtils.js +12 -0
- package/utils/dataTableUtils.js +353 -0
|
@@ -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();
|
|
@@ -255,13 +255,15 @@ class RequestService {
|
|
|
255
255
|
//winston.info("main_flow_cache_3 route");
|
|
256
256
|
|
|
257
257
|
// Find request
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
// Cache disabled: a stale cached document can have an outdated __v and cause
|
|
259
|
+
// Mongoose VersionError on save ("No matching document found for id ... version N").
|
|
260
|
+
// let query = Request.findOne({ request_id, id_project });
|
|
261
|
+
// if (cacheEnabler.request) {
|
|
262
|
+
// query = query.cache(cacheUtil.defaultTTL, id_project + ":requests:request_id:" + request_id + ":simple");
|
|
263
|
+
// winston.debug('request cache enabled');
|
|
264
|
+
// }
|
|
265
|
+
// const request = await query.exec();
|
|
266
|
+
const request = await Request.findOne({ request_id, id_project }).exec();
|
|
265
267
|
|
|
266
268
|
if (!request) {
|
|
267
269
|
throw new Error(`Request not found: ${request_id}`);
|
|
@@ -401,15 +403,18 @@ class RequestService {
|
|
|
401
403
|
// winston.debug("request_id", request_id);
|
|
402
404
|
// winston.debug("newstatus", newstatus);
|
|
403
405
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return q.exec(function (err, request) {
|
|
406
|
+
// Cache disabled: a stale cached document can have an outdated __v and cause
|
|
407
|
+
// Mongoose VersionError on save ("No matching document found for id ... version N").
|
|
408
|
+
// let q = Request
|
|
409
|
+
// .findOne({ request_id: request_id, id_project: id_project });
|
|
410
|
+
// if (cacheEnabler.request) {
|
|
411
|
+
// q.cache(cacheUtil.defaultTTL, id_project + ":requests:request_id:" + request_id + ":simple");
|
|
412
|
+
// winston.debug('request cache enabled');
|
|
413
|
+
// }
|
|
414
|
+
// return q.exec(function (err, request) {
|
|
415
|
+
return Request
|
|
416
|
+
.findOne({ request_id: request_id, id_project: id_project })
|
|
417
|
+
.exec(function (err, request) {
|
|
413
418
|
|
|
414
419
|
if (err) {
|
|
415
420
|
winston.error(err);
|