@steedos-labs/plugin-workflow 3.0.21 → 3.0.23
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/APPROVAL_COMMENTS_OPERATIONS.md +673 -0
- package/APPROVAL_COMMENTS_UPGRADE.md +854 -0
- package/main/default/manager/handlers_manager.js +11 -12
- package/main/default/manager/uuflow_manager.js +4 -1
- package/main/default/manager/workflow_manager.js +12 -0
- package/main/default/pages/instance_tasks_detail.page.amis.json +16 -0
- package/main/default/pages/page_instance_print.page.amis.json +1 -1
- package/main/default/routes/api_files.router.js +15 -1
- package/main/default/routes/api_workflow_chart.router.js +41 -9
- package/main/default/routes/api_workflow_next_step.router.js +1 -1
- package/main/default/routes/api_workflow_next_step_users.router.js +7 -2
- package/main/default/routes/flow_form_design.ejs +21 -1
- package/main/default/triggers/amis_form_design.trigger.js +6 -4
- package/main/default/utils/designerManager.js +1 -2
- package/package.json +1 -1
- package/public/workflow/index.css +3 -0
- package/src/rests/approvalCommentsConsole.js +460 -0
- package/src/rests/index.js +5 -1
- package/src/rests/integrationTestApprovalComments.js +96 -0
- package/src/rests/migrateApprovalCommentsField.js +536 -0
- package/src/rests/rollbackApprovalCommentsField.js +319 -0
- package/src/util/templateConverter.js +8 -2
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Author: Copilot
|
|
3
|
+
* @Date: 2026-01-28
|
|
4
|
+
* @Description: Transform old approval comments field configurations to new format
|
|
5
|
+
*
|
|
6
|
+
* This endpoint scans forms collection and transforms fields with approval comments formulas
|
|
7
|
+
* from old format to new steedos-field format with config object.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { MongoClient } = require('mongodb');
|
|
11
|
+
|
|
12
|
+
// MongoDB connection
|
|
13
|
+
let client = null;
|
|
14
|
+
let db = null;
|
|
15
|
+
|
|
16
|
+
async function connectToMongoDB() {
|
|
17
|
+
if (db) {
|
|
18
|
+
return db;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const mongoUrl = process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/steedos';
|
|
22
|
+
try {
|
|
23
|
+
client = new MongoClient(mongoUrl);
|
|
24
|
+
await client.connect();
|
|
25
|
+
|
|
26
|
+
const dbName = mongoUrl.split('/').pop().split('?')[0];
|
|
27
|
+
db = client.db(dbName);
|
|
28
|
+
return db;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Failed to connect to MongoDB:', error.message);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getCollection(name) {
|
|
36
|
+
const database = await connectToMongoDB();
|
|
37
|
+
return database.collection(name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a field value matches approval comments patterns
|
|
42
|
+
*/
|
|
43
|
+
function matchesApprovalPattern(value) {
|
|
44
|
+
if (!value || typeof value !== 'string') {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const patterns = [
|
|
49
|
+
/\{traces\./,
|
|
50
|
+
/\$\{traces\./,
|
|
51
|
+
/\{signature\.traces\./,
|
|
52
|
+
/\$\{signature\.traces\./,
|
|
53
|
+
/\{yijianlan:/
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
return patterns.some(pattern => pattern.test(value));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse yijianlan object string (non-standard JSON with unquoted keys)
|
|
61
|
+
* Example: {yijianlan:{step:'部门经理审核',default:'已阅'}}
|
|
62
|
+
*/
|
|
63
|
+
function parseYijianlan(str) {
|
|
64
|
+
try {
|
|
65
|
+
// Extract yijianlan content
|
|
66
|
+
const match = str.match(/\{yijianlan:\s*(\{[^}]+\})\s*\}/);
|
|
67
|
+
if (!match) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const objStr = match[1];
|
|
72
|
+
// Convert to valid JSON by adding quotes to unquoted keys and converting single quotes to double quotes
|
|
73
|
+
// Note: This assumes the yijianlan content is in the expected format
|
|
74
|
+
const jsonStr = objStr
|
|
75
|
+
.replace(/(\w+):/g, '"$1":') // Add quotes to keys
|
|
76
|
+
.replace(/'/g, '"'); // Convert single quotes to double quotes
|
|
77
|
+
return JSON.parse(jsonStr);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Log more detailed error for troubleshooting
|
|
80
|
+
console.error('Error parsing yijianlan:', error.message, 'Input:', str);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract step name from formula/default_value
|
|
87
|
+
*/
|
|
88
|
+
function extractStepName(value) {
|
|
89
|
+
// Handle yijianlan format
|
|
90
|
+
if (value.includes('yijianlan:')) {
|
|
91
|
+
const yijianlan = parseYijianlan(value);
|
|
92
|
+
if (yijianlan && yijianlan.step) {
|
|
93
|
+
return yijianlan.step;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle ${signature.traces.XXX} or {signature.traces.XXX}
|
|
98
|
+
let match = value.match(/\$?\{signature\.traces\.([^}]+)\}/);
|
|
99
|
+
if (match) {
|
|
100
|
+
return match[1];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle ${traces.XXX} or {traces.XXX}
|
|
104
|
+
match = value.match(/\$?\{traces\.([^}]+)\}/);
|
|
105
|
+
if (match) {
|
|
106
|
+
return match[1];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse special boolean logic for show_cc and show_handler
|
|
114
|
+
*/
|
|
115
|
+
function parseShowOptions(yijianlan) {
|
|
116
|
+
if (!yijianlan) {
|
|
117
|
+
return { show_cc: true, show_handler: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (yijianlan.only_cc === true || yijianlan.only_cc === 'true') {
|
|
121
|
+
return { show_cc: true, show_handler: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (yijianlan.only_handler === true || yijianlan.only_handler === 'true') {
|
|
125
|
+
return { show_cc: false, show_handler: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { show_cc: true, show_handler: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Transform a single field
|
|
133
|
+
*/
|
|
134
|
+
function transformField(field) {
|
|
135
|
+
// Check formula and default_value only
|
|
136
|
+
const formulaValue = field.formula || field.default_value;
|
|
137
|
+
|
|
138
|
+
if (!formulaValue || !matchesApprovalPattern(formulaValue)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Already transformed
|
|
143
|
+
if (field.__approval_comments_transformed) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const stepName = extractStepName(formulaValue);
|
|
148
|
+
if (!stepName) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Parse yijianlan for additional config
|
|
153
|
+
const yijianlan = formulaValue.includes('yijianlan:') ? parseYijianlan(formulaValue) : null;
|
|
154
|
+
const showOptions = parseShowOptions(yijianlan);
|
|
155
|
+
|
|
156
|
+
// Check if it's signature.traces (image sign)
|
|
157
|
+
const showImageSign = formulaValue.includes('signature.traces') ||
|
|
158
|
+
(yijianlan && yijianlan.image_sign === true) ||
|
|
159
|
+
false;
|
|
160
|
+
|
|
161
|
+
// Get default value from yijianlan
|
|
162
|
+
const defaultValue = yijianlan && yijianlan.default ? yijianlan.default : undefined;
|
|
163
|
+
|
|
164
|
+
// Get label from field name or code only
|
|
165
|
+
const label = field.name || field.code;
|
|
166
|
+
|
|
167
|
+
// Build the step config
|
|
168
|
+
const step = {
|
|
169
|
+
name: stepName,
|
|
170
|
+
show_cc: showOptions.show_cc,
|
|
171
|
+
show_handler: showOptions.show_handler,
|
|
172
|
+
show_image_sign: showImageSign
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (defaultValue !== undefined) {
|
|
176
|
+
step.default = defaultValue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Build update object
|
|
180
|
+
const update = {
|
|
181
|
+
type: 'steedos-field',
|
|
182
|
+
__approval_comments_transformed: true,
|
|
183
|
+
__approval_comments_original_type: field.type || 'input', // Store original type for zero-loss rollback
|
|
184
|
+
config: {
|
|
185
|
+
type: 'approval_comments',
|
|
186
|
+
label: label,
|
|
187
|
+
object: '',
|
|
188
|
+
name: field.code || field.name,
|
|
189
|
+
steps: [step]
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Backup original formula if exists
|
|
194
|
+
if (field.formula) {
|
|
195
|
+
update.__approval_comments_original_formula = field.formula;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Backup original default_value if exists
|
|
199
|
+
if (field.default_value) {
|
|
200
|
+
update.__approval_comments_original_default_value = field.default_value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Rename _amisField to __approval_comments_original__amisField if it exists
|
|
204
|
+
if (field._amisField) {
|
|
205
|
+
update.__approval_comments_original__amisField = field._amisField;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return update;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Transform a single amis schema field node
|
|
213
|
+
*/
|
|
214
|
+
function transformAmisSchemaField(node) {
|
|
215
|
+
// Check if node has a value property that matches approval patterns
|
|
216
|
+
if (!node || !node.value || typeof node.value !== 'string') {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!matchesApprovalPattern(node.value)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const stepName = extractStepName(node.value);
|
|
225
|
+
if (!stepName) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Parse yijianlan for additional config
|
|
230
|
+
const yijianlan = node.value.includes('yijianlan:') ? parseYijianlan(node.value) : null;
|
|
231
|
+
const showOptions = parseShowOptions(yijianlan);
|
|
232
|
+
|
|
233
|
+
// Check if it's signature.traces (image sign)
|
|
234
|
+
const showImageSign = node.value.includes('signature.traces') ||
|
|
235
|
+
(yijianlan && yijianlan.image_sign === true) ||
|
|
236
|
+
false;
|
|
237
|
+
|
|
238
|
+
// Get default value from yijianlan
|
|
239
|
+
const defaultValue = yijianlan && yijianlan.default ? yijianlan.default : undefined;
|
|
240
|
+
|
|
241
|
+
// Build the step config
|
|
242
|
+
const step = {
|
|
243
|
+
name: stepName,
|
|
244
|
+
show_cc: showOptions.show_cc,
|
|
245
|
+
show_handler: showOptions.show_handler,
|
|
246
|
+
show_image_sign: showImageSign
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (defaultValue !== undefined) {
|
|
250
|
+
step.default = defaultValue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Build transformed node - keep all original properties except value
|
|
254
|
+
const transformed = {
|
|
255
|
+
...node,
|
|
256
|
+
type: 'sfield-approvalcomments',
|
|
257
|
+
__approval_comments_original_type: node.type, // Store original type for zero-loss rollback
|
|
258
|
+
config: {
|
|
259
|
+
type: 'approval_comments',
|
|
260
|
+
label: node.label || node.name,
|
|
261
|
+
object: '',
|
|
262
|
+
name: node.name,
|
|
263
|
+
steps: [step]
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Remove value property from transformed node
|
|
268
|
+
delete transformed.value;
|
|
269
|
+
|
|
270
|
+
return transformed;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Process amis_schema string - parse, transform body nodes, and stringify
|
|
275
|
+
*/
|
|
276
|
+
function processAmisSchema(amisSchemaStr) {
|
|
277
|
+
if (!amisSchemaStr || typeof amisSchemaStr !== 'string') {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
// Parse the JSON string
|
|
283
|
+
const schema = JSON.parse(amisSchemaStr);
|
|
284
|
+
|
|
285
|
+
// Only process if schema has a body array
|
|
286
|
+
if (!schema.body || !Array.isArray(schema.body)) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let hasChanges = false;
|
|
291
|
+
|
|
292
|
+
// Process only first-level body nodes
|
|
293
|
+
for (let i = 0; i < schema.body.length; i++) {
|
|
294
|
+
const node = schema.body[i];
|
|
295
|
+
const transformed = transformAmisSchemaField(node);
|
|
296
|
+
|
|
297
|
+
if (transformed) {
|
|
298
|
+
schema.body[i] = transformed;
|
|
299
|
+
hasChanges = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Return stringified schema if there were changes
|
|
304
|
+
if (hasChanges) {
|
|
305
|
+
return JSON.stringify(schema);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error('Error processing amis_schema:', error.message, error.stack);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Process fields array and return bulk operations
|
|
317
|
+
*/
|
|
318
|
+
function processFieldsArray(fields, pathPrefix) {
|
|
319
|
+
const updates = [];
|
|
320
|
+
const unsets = [];
|
|
321
|
+
|
|
322
|
+
if (!Array.isArray(fields)) {
|
|
323
|
+
return { updates, unsets };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < fields.length; i++) {
|
|
327
|
+
const field = fields[i];
|
|
328
|
+
const transformation = transformField(field);
|
|
329
|
+
|
|
330
|
+
if (transformation) {
|
|
331
|
+
updates.push({
|
|
332
|
+
index: i,
|
|
333
|
+
path: `${pathPrefix}.${i}`,
|
|
334
|
+
update: transformation,
|
|
335
|
+
fieldId: field._id || field.code || field.name, // Track field for counting
|
|
336
|
+
hasAmisField: !!field._amisField // Track if we need to unset _amisField
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Unset original properties that are being backed up
|
|
340
|
+
if (field._amisField) {
|
|
341
|
+
unsets.push(`${pathPrefix}.${i}._amisField`);
|
|
342
|
+
}
|
|
343
|
+
if (field.formula) {
|
|
344
|
+
unsets.push(`${pathPrefix}.${i}.formula`);
|
|
345
|
+
}
|
|
346
|
+
if (field.default_value) {
|
|
347
|
+
unsets.push(`${pathPrefix}.${i}.default_value`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { updates, unsets };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = {
|
|
356
|
+
rest: {
|
|
357
|
+
method: 'GET',
|
|
358
|
+
fullPath: '/api/workflow/migrateApprovalCommentsField'
|
|
359
|
+
},
|
|
360
|
+
params: {
|
|
361
|
+
batchSize: { type: 'number', optional: true, convert: true },
|
|
362
|
+
dryRun: { type: 'boolean', optional: true, convert: true },
|
|
363
|
+
fid: { type: 'string', optional: true } // Flow ID to process single flow
|
|
364
|
+
},
|
|
365
|
+
async handler(ctx) {
|
|
366
|
+
const { user } = ctx.meta;
|
|
367
|
+
|
|
368
|
+
// Check if user exists and is space admin
|
|
369
|
+
if (!user || user.is_space_admin !== true) {
|
|
370
|
+
throw new Error('只有管理员才能执行此操作');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const { batchSize = 500, dryRun = false, fid } = ctx.params;
|
|
374
|
+
|
|
375
|
+
const formsCollection = await getCollection('forms');
|
|
376
|
+
|
|
377
|
+
// Build query filter
|
|
378
|
+
const query = { state: 'enabled' }; // Only process enabled forms
|
|
379
|
+
|
|
380
|
+
// If fid is provided, query flows collection to get form ID
|
|
381
|
+
if (fid) {
|
|
382
|
+
const flowsCollection = await getCollection('flows');
|
|
383
|
+
const flow = await flowsCollection.findOne({ _id: fid });
|
|
384
|
+
|
|
385
|
+
if (!flow) {
|
|
386
|
+
throw new Error(`Flow with ID ${fid} not found`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!flow.form) {
|
|
390
|
+
throw new Error(`Flow ${fid} does not have a form associated`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Add form ID filter
|
|
394
|
+
query._id = flow.form;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Use cursor for memory efficiency
|
|
398
|
+
const cursor = formsCollection.find(query);
|
|
399
|
+
|
|
400
|
+
const results = {
|
|
401
|
+
totalForms: 0,
|
|
402
|
+
totalFields: 0,
|
|
403
|
+
errors: []
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const bulkOperations = [];
|
|
407
|
+
|
|
408
|
+
// Process forms one by one using cursor
|
|
409
|
+
while (await cursor.hasNext()) {
|
|
410
|
+
const form = await cursor.next();
|
|
411
|
+
try {
|
|
412
|
+
let formUpdated = false;
|
|
413
|
+
const formUpdate = {};
|
|
414
|
+
const formUnset = {};
|
|
415
|
+
let formFieldCount = 0; // Track actual fields transformed in this form
|
|
416
|
+
|
|
417
|
+
// Process current.fields
|
|
418
|
+
if (form.current && Array.isArray(form.current.fields)) {
|
|
419
|
+
const { updates, unsets } = processFieldsArray(form.current.fields, 'current.fields');
|
|
420
|
+
|
|
421
|
+
if (updates.length > 0) {
|
|
422
|
+
formUpdated = true;
|
|
423
|
+
formFieldCount += updates.length; // Count fields, not operations
|
|
424
|
+
|
|
425
|
+
// Build $set updates for current.fields
|
|
426
|
+
for (const update of updates) {
|
|
427
|
+
for (const key in update.update) {
|
|
428
|
+
formUpdate[`current.fields.${update.index}.${key}`] = update.update[key];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Build $unset for _amisField
|
|
433
|
+
for (const path of unsets) {
|
|
434
|
+
formUnset[path] = '';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Process historys[].fields
|
|
440
|
+
if (Array.isArray(form.historys)) {
|
|
441
|
+
for (let h = 0; h < form.historys.length; h++) {
|
|
442
|
+
const history = form.historys[h];
|
|
443
|
+
if (Array.isArray(history.fields)) {
|
|
444
|
+
const { updates, unsets } = processFieldsArray(
|
|
445
|
+
history.fields,
|
|
446
|
+
`historys.${h}.fields`
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if (updates.length > 0) {
|
|
450
|
+
formUpdated = true;
|
|
451
|
+
formFieldCount += updates.length; // Count fields, not operations
|
|
452
|
+
|
|
453
|
+
// Build $set updates for historys.fields
|
|
454
|
+
for (const update of updates) {
|
|
455
|
+
for (const key in update.update) {
|
|
456
|
+
formUpdate[`historys.${h}.fields.${update.index}.${key}`] = update.update[key];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Build $unset for _amisField
|
|
461
|
+
for (const path of unsets) {
|
|
462
|
+
formUnset[path] = '';
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Process root amis_schema (not current.amis_schema or historys[*].amis_schema)
|
|
470
|
+
if (form.amis_schema && typeof form.amis_schema === 'string') {
|
|
471
|
+
const transformedSchema = processAmisSchema(form.amis_schema);
|
|
472
|
+
|
|
473
|
+
if (transformedSchema) {
|
|
474
|
+
formUpdated = true;
|
|
475
|
+
|
|
476
|
+
// Backup original amis_schema
|
|
477
|
+
formUpdate.__approval_comments_original__amis_schema = form.amis_schema;
|
|
478
|
+
|
|
479
|
+
// Set transformed amis_schema
|
|
480
|
+
formUpdate.amis_schema = transformedSchema;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (formUpdated) {
|
|
485
|
+
results.totalForms++;
|
|
486
|
+
results.totalFields += formFieldCount; // Add actual field count
|
|
487
|
+
|
|
488
|
+
if (!dryRun) {
|
|
489
|
+
const updateDoc = { $set: formUpdate };
|
|
490
|
+
if (Object.keys(formUnset).length > 0) {
|
|
491
|
+
updateDoc.$unset = formUnset;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
bulkOperations.push({
|
|
495
|
+
updateOne: {
|
|
496
|
+
filter: { _id: form._id },
|
|
497
|
+
update: updateDoc
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Execute bulk write in batches
|
|
502
|
+
if (bulkOperations.length >= batchSize) {
|
|
503
|
+
await formsCollection.bulkWrite(bulkOperations, { ordered: false });
|
|
504
|
+
bulkOperations.length = 0; // Clear array
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error(`Error processing form ${form._id}:`, error);
|
|
510
|
+
results.errors.push({
|
|
511
|
+
formId: form._id,
|
|
512
|
+
formName: form.name,
|
|
513
|
+
error: error.message
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Execute remaining bulk operations
|
|
519
|
+
if (!dryRun && bulkOperations.length > 0) {
|
|
520
|
+
try {
|
|
521
|
+
await formsCollection.bulkWrite(bulkOperations, { ordered: false });
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error('Error executing final bulk write:', error);
|
|
524
|
+
results.errors.push({
|
|
525
|
+
error: 'Final bulk write failed: ' + error.message
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
...results,
|
|
532
|
+
dryRun,
|
|
533
|
+
message: dryRun ? 'Dry run completed - no changes made' : 'Transformation completed'
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
};
|