@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.
@@ -0,0 +1,319 @@
1
+ /*
2
+ * @Author: Copilot
3
+ * @Date: 2026-01-28
4
+ * @Description: Rollback approval comments field transformations
5
+ *
6
+ * This endpoint reverts fields that were transformed by the transform endpoint
7
+ * back to their original input type format.
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
+ * Process fields array and return bulk operations for rollback
42
+ * Implements field-level error handling for silent failure
43
+ */
44
+ function processFieldsArrayForRollback(fields, pathPrefix, formId, formName) {
45
+ const updates = [];
46
+ const unsets = [];
47
+ let fieldCount = 0; // Track actual fields processed
48
+ const fieldErrors = []; // Track field-specific errors
49
+
50
+ if (!Array.isArray(fields)) {
51
+ return { updates, unsets, fieldCount, fieldErrors };
52
+ }
53
+
54
+ for (let i = 0; i < fields.length; i++) {
55
+ const field = fields[i];
56
+
57
+ // Only rollback fields that were transformed (Idempotency principle)
58
+ if (field.__approval_comments_transformed === true) {
59
+ try {
60
+ fieldCount++; // Count this field
61
+
62
+ // Zero Loss principle: Restore type with priority to stored original type
63
+ const originalType = field.__approval_comments_original_type || 'input';
64
+ updates.push({
65
+ index: i,
66
+ path: `${pathPrefix}.${i}.type`,
67
+ value: originalType
68
+ });
69
+
70
+ // Restore original formula if exists
71
+ if (field.__approval_comments_original_formula) {
72
+ updates.push({
73
+ index: i,
74
+ path: `${pathPrefix}.${i}.formula`,
75
+ value: field.__approval_comments_original_formula
76
+ });
77
+ }
78
+
79
+ // Restore original default_value if exists
80
+ if (field.__approval_comments_original_default_value) {
81
+ updates.push({
82
+ index: i,
83
+ path: `${pathPrefix}.${i}.default_value`,
84
+ value: field.__approval_comments_original_default_value
85
+ });
86
+ }
87
+
88
+ // Rename __approval_comments_original__amisField back to _amisField if it exists
89
+ if (field.__approval_comments_original__amisField) {
90
+ updates.push({
91
+ index: i,
92
+ path: `${pathPrefix}.${i}._amisField`,
93
+ value: field.__approval_comments_original__amisField
94
+ });
95
+ }
96
+
97
+ // Remove added properties
98
+ unsets.push(`${pathPrefix}.${i}.__approval_comments_transformed`);
99
+ unsets.push(`${pathPrefix}.${i}.config`);
100
+ unsets.push(`${pathPrefix}.${i}.__approval_comments_original__amisField`);
101
+ unsets.push(`${pathPrefix}.${i}.__approval_comments_original_type`); // Remove backup
102
+ unsets.push(`${pathPrefix}.${i}.__approval_comments_original_formula`); // Remove backup
103
+ unsets.push(`${pathPrefix}.${i}.__approval_comments_original_default_value`); // Remove backup
104
+ } catch (error) {
105
+ // Silent Failure principle: Record error but continue processing
106
+ fieldErrors.push({
107
+ formId: formId,
108
+ formName: formName,
109
+ fieldPath: `${pathPrefix}.${i}`,
110
+ fieldCode: field.code || field.name || 'unknown',
111
+ error: error.message
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ return { updates, unsets, fieldCount, fieldErrors };
118
+ }
119
+
120
+ module.exports = {
121
+ rest: {
122
+ method: 'GET',
123
+ fullPath: '/api/workflow/rollbackApprovalCommentsField'
124
+ },
125
+ params: {
126
+ batchSize: { type: 'number', optional: true, convert: true },
127
+ dryRun: { type: 'boolean', optional: true, convert: true },
128
+ fid: { type: 'string', optional: true } // Flow ID to rollback single flow
129
+ },
130
+ async handler(ctx) {
131
+ const { user } = ctx.meta;
132
+
133
+ // Check if user exists and is space admin
134
+ if (!user || user.is_space_admin !== true) {
135
+ throw new Error('只有管理员才能执行此操作');
136
+ }
137
+
138
+ const { batchSize = 500, dryRun = false, fid } = ctx.params;
139
+
140
+ const formsCollection = await getCollection('forms');
141
+
142
+ // Build query filter
143
+ const query = {
144
+ state: 'enabled', // Only process enabled forms
145
+ $or: [
146
+ { 'current.fields.__approval_comments_transformed': true },
147
+ { 'historys.fields.__approval_comments_transformed': true }
148
+ ]
149
+ };
150
+
151
+ // If fid is provided, query flows collection to get form ID
152
+ if (fid) {
153
+ const flowsCollection = await getCollection('flows');
154
+ const flow = await flowsCollection.findOne({ _id: fid });
155
+
156
+ if (!flow) {
157
+ throw new Error(`Flow with ID ${fid} not found`);
158
+ }
159
+
160
+ if (!flow.form) {
161
+ throw new Error(`Flow ${fid} does not have a form associated`);
162
+ }
163
+
164
+ // Add form ID filter
165
+ query._id = flow.form;
166
+ }
167
+
168
+ // Use cursor to find forms with transformed fields
169
+ const cursor = formsCollection.find(query);
170
+
171
+ const results = {
172
+ totalForms: 0,
173
+ totalFields: 0,
174
+ errors: []
175
+ };
176
+
177
+ const bulkOperations = [];
178
+
179
+ // Process forms one by one using cursor
180
+ while (await cursor.hasNext()) {
181
+ const form = await cursor.next();
182
+ try {
183
+ let formUpdated = false;
184
+ const formUpdate = {};
185
+ const formUnset = {};
186
+ let formFieldCount = 0; // Track actual fields rolled back
187
+
188
+ // Process current.fields
189
+ if (form.current && Array.isArray(form.current.fields)) {
190
+ const { updates, unsets, fieldCount, fieldErrors } = processFieldsArrayForRollback(
191
+ form.current.fields,
192
+ 'current.fields',
193
+ form._id,
194
+ form.name
195
+ );
196
+
197
+ // Add field-level errors to results
198
+ if (fieldErrors && fieldErrors.length > 0) {
199
+ results.errors.push(...fieldErrors);
200
+ }
201
+
202
+ if (updates.length > 0 || unsets.length > 0) {
203
+ formUpdated = true;
204
+ formFieldCount += fieldCount; // Add field count, not operation count
205
+
206
+ // Build $set updates
207
+ for (const update of updates) {
208
+ formUpdate[update.path] = update.value;
209
+ }
210
+
211
+ // Build $unset
212
+ for (const path of unsets) {
213
+ formUnset[path] = '';
214
+ }
215
+ }
216
+ }
217
+
218
+ // Process historys[].fields (Full Coverage principle)
219
+ if (Array.isArray(form.historys)) {
220
+ for (let h = 0; h < form.historys.length; h++) {
221
+ const history = form.historys[h];
222
+ if (Array.isArray(history.fields)) {
223
+ const { updates, unsets, fieldCount, fieldErrors } = processFieldsArrayForRollback(
224
+ history.fields,
225
+ `historys.${h}.fields`,
226
+ form._id,
227
+ form.name
228
+ );
229
+
230
+ // Add field-level errors to results
231
+ if (fieldErrors && fieldErrors.length > 0) {
232
+ results.errors.push(...fieldErrors);
233
+ }
234
+
235
+ if (updates.length > 0 || unsets.length > 0) {
236
+ formUpdated = true;
237
+ formFieldCount += fieldCount; // Add field count, not operation count
238
+
239
+ // Build $set updates
240
+ for (const update of updates) {
241
+ formUpdate[update.path] = update.value;
242
+ }
243
+
244
+ // Build $unset
245
+ for (const path of unsets) {
246
+ formUnset[path] = '';
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ // Restore root amis_schema from backup if exists
254
+ if (form.__approval_comments_original__amis_schema) {
255
+ formUpdated = true;
256
+
257
+ // Restore original amis_schema
258
+ formUpdate.amis_schema = form.__approval_comments_original__amis_schema;
259
+
260
+ // Remove backup
261
+ formUnset.__approval_comments_original__amis_schema = '';
262
+ }
263
+
264
+ if (formUpdated) {
265
+ results.totalForms++;
266
+ results.totalFields += formFieldCount; // Add actual field count
267
+
268
+ if (!dryRun) {
269
+ const updateDoc = {};
270
+ if (Object.keys(formUpdate).length > 0) {
271
+ updateDoc.$set = formUpdate;
272
+ }
273
+ if (Object.keys(formUnset).length > 0) {
274
+ updateDoc.$unset = formUnset;
275
+ }
276
+
277
+ bulkOperations.push({
278
+ updateOne: {
279
+ filter: { _id: form._id },
280
+ update: updateDoc
281
+ }
282
+ });
283
+
284
+ // Execute bulk write in batches
285
+ if (bulkOperations.length >= batchSize) {
286
+ await formsCollection.bulkWrite(bulkOperations, { ordered: false });
287
+ bulkOperations.length = 0; // Clear array
288
+ }
289
+ }
290
+ }
291
+ } catch (error) {
292
+ console.error(`Error processing form ${form._id}:`, error);
293
+ results.errors.push({
294
+ formId: form._id,
295
+ formName: form.name,
296
+ error: error.message
297
+ });
298
+ }
299
+ }
300
+
301
+ // Execute remaining bulk operations
302
+ if (!dryRun && bulkOperations.length > 0) {
303
+ try {
304
+ await formsCollection.bulkWrite(bulkOperations, { ordered: false });
305
+ } catch (error) {
306
+ console.error('Error executing final bulk write:', error);
307
+ results.errors.push({
308
+ error: 'Final bulk write failed: ' + error.message
309
+ });
310
+ }
311
+ }
312
+
313
+ return {
314
+ ...results,
315
+ dryRun,
316
+ message: dryRun ? 'Dry run completed - no changes made' : 'Rollback completed'
317
+ };
318
+ }
319
+ };
@@ -290,15 +290,21 @@ function convertTemplate(content, fields, isPrint) {
290
290
  newContent = newContent.replace(/\{\{afFieldLabelText\s+name=["'](.+?)["']\}\}/g, '$1');
291
291
 
292
292
  // --- Sub-phase 2d: Convert instanceSignText ---
293
- newContent = newContent.replace(/\{\{>\s*instanceSignText\s+([^}]+)\}\}/g, (match, args) => {
293
+ newContent = newContent.replace(/\{\{>\s*instanceSignText\s+([\s\S]+?)\}\}/g, (match, args) => {
294
294
  const argMap = parseArgs(args);
295
295
  const fieldName = argMap.name;
296
296
  if (!fieldName) return match;
297
297
 
298
298
  const fieldDef = findFieldDef(fields, fieldName);
299
299
  let stepName = "";
300
+
301
+ let formula = argMap.field_formula;
300
302
  if (fieldDef && fieldDef.formula) {
301
- const formulaMatch = fieldDef.formula.match(/signature\.traces\.([^}]+)/);
303
+ formula = fieldDef.formula;
304
+ }
305
+
306
+ if (formula) {
307
+ const formulaMatch = formula.match(/signature\.traces\.([^}]+)/);
302
308
  if (formulaMatch) {
303
309
  stepName = formulaMatch[1].replace('}', '');
304
310
  }