@steedos-labs/plugin-workflow 3.0.14 → 3.0.16

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/run.js ADDED
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Workflow Template Export/Import Script
5
+ *
6
+ * This script exports and imports workflow templates (instance_template and print_template)
7
+ * from MongoDB to files for Blaze-to-Liquid conversion.
8
+ *
9
+ * Usage:
10
+ * node run.js export [flowId] [outputDir]
11
+ * node run.js import <flowId> [templateType] [inputDir]
12
+ *
13
+ * Examples:
14
+ * node run.js export # Export all flows
15
+ * node run.js export abc123 # Export specific flow
16
+ * node run.js export abc123 /custom/path # Export to custom directory
17
+ * node run.js import abc123 # Import both templates
18
+ * node run.js import abc123 instance_template # Import only instance_template
19
+ */
20
+ require('dotenv-flow').config({});
21
+ const { MongoClient } = require('mongodb');
22
+ const path = require('path');
23
+ const fs = require('fs').promises;
24
+
25
+ // MongoDB connection
26
+ let client = null;
27
+ let db = null;
28
+
29
+ // Connect to MongoDB using native driver
30
+ async function connectToMongoDB() {
31
+ if (db) {
32
+ return db;
33
+ }
34
+
35
+ const mongoUrl = process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/steedos-plugins';
36
+ console.log('mongoUrl', mongoUrl)
37
+ try {
38
+ client = new MongoClient(mongoUrl, {
39
+ useNewUrlParser: true,
40
+ useUnifiedTopology: true,
41
+ directConnection: true
42
+ });
43
+ await client.connect();
44
+
45
+ // Extract database name from URL
46
+ const dbName = mongoUrl.split('/').pop().split('?')[0];
47
+ db = client.db(dbName);
48
+
49
+ console.log(`Connected to MongoDB: ${dbName}`);
50
+ return db;
51
+ } catch (error) {
52
+ console.error('Failed to connect to MongoDB:', error.message);
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ // Close MongoDB connection
58
+ async function closeMongoDB() {
59
+ if (client) {
60
+ await client.close();
61
+ client = null;
62
+ db = null;
63
+ }
64
+ }
65
+
66
+ // Get collection from MongoDB
67
+ async function getCollection(name) {
68
+ const database = await connectToMongoDB();
69
+ return database.collection(name);
70
+ }
71
+
72
+ // Export templates to .blaze files
73
+ async function exportTemplates(flowId, outputDir) {
74
+ try {
75
+ const baseDir = outputDir || path.join(process.cwd(), 'flows-template');
76
+
77
+ // Create directory
78
+ try {
79
+ await fs.mkdir(baseDir, { recursive: true });
80
+ } catch (mkdirError) {
81
+ if (mkdirError.code !== 'EEXIST') {
82
+ throw new Error(`Failed to create directory ${baseDir}: ${mkdirError.message}`);
83
+ }
84
+ }
85
+
86
+ const flowsCollection = await getCollection('flows');
87
+ const formsCollection = await getCollection('forms');
88
+
89
+ // Build query
90
+ const query = {};
91
+ if (flowId) {
92
+ // Export specific flow by ID
93
+ query._id = flowId;
94
+ } else {
95
+ // Export all flows with templates (when flowId is not specified)
96
+ query.$or = [
97
+ { instance_template: { $exists: true, $ne: null, $ne: '' } },
98
+ { print_template: { $exists: true, $ne: null, $ne: '' } }
99
+ ];
100
+ }
101
+
102
+ const flows = await flowsCollection.find(query, {
103
+ projection: {
104
+ _id: 1,
105
+ name: 1,
106
+ instance_template: 1,
107
+ print_template: 1,
108
+ form: 1
109
+ }
110
+ }).toArray();
111
+
112
+ if (flows.length === 0) {
113
+ console.log('No flows found with templates.');
114
+ return;
115
+ }
116
+
117
+ if (flowId) {
118
+ console.log(`Found flow: ${flows[0]?.name || flowId}`);
119
+ } else {
120
+ console.log(`Found ${flows.length} flow(s) with templates to export.`);
121
+ }
122
+
123
+ const exportSummary = [];
124
+
125
+ for (const flow of flows) {
126
+ console.log(`\nExporting flow: ${flow.name} (${flow._id})`);
127
+
128
+ let fileCount = 0;
129
+ const flowSummary = {
130
+ id: flow._id,
131
+ name: flow.name,
132
+ templates: []
133
+ };
134
+
135
+ // Export instance_template
136
+ if (flow.instance_template) {
137
+ try {
138
+ const fileName = `${flow._id}.instance_template.blaze`;
139
+ const filePath = path.join(baseDir, fileName);
140
+ await fs.writeFile(filePath, flow.instance_template, 'utf8');
141
+ console.log(` ✓ ${fileName}`);
142
+ fileCount++;
143
+ flowSummary.templates.push('instance_template');
144
+ } catch (error) {
145
+ console.error(` ✗ Failed to write instance_template: ${error.message}`);
146
+ }
147
+ }
148
+
149
+ // Export print_template
150
+ if (flow.print_template) {
151
+ try {
152
+ const fileName = `${flow._id}.print_template.blaze`;
153
+ const filePath = path.join(baseDir, fileName);
154
+ await fs.writeFile(filePath, flow.print_template, 'utf8');
155
+ console.log(` ✓ ${fileName}`);
156
+ fileCount++;
157
+ flowSummary.templates.push('print_template');
158
+ } catch (error) {
159
+ console.error(` ✗ Failed to write print_template: ${error.message}`);
160
+ }
161
+ }
162
+
163
+ // Export field definitions
164
+ if (flow.form) {
165
+ const form = await formsCollection.findOne(
166
+ { _id: flow.form },
167
+ { projection: { 'current.fields': 1 } }
168
+ );
169
+
170
+ if (form && form.current && form.current.fields) {
171
+ try {
172
+ const fileName = `${flow._id}.fields.json`;
173
+ const filePath = path.join(baseDir, fileName);
174
+ await fs.writeFile(
175
+ filePath,
176
+ JSON.stringify(form.current.fields, null, 2),
177
+ 'utf8'
178
+ );
179
+ console.log(` ✓ ${fileName}`);
180
+ fileCount++;
181
+ flowSummary.templates.push('fields');
182
+ } catch (error) {
183
+ console.error(` ✗ Failed to write fields.json: ${error.message}`);
184
+ }
185
+ }
186
+ }
187
+
188
+ if (fileCount > 0) {
189
+ console.log(` Exported ${fileCount} file(s)`);
190
+ exportSummary.push(flowSummary);
191
+ }
192
+ }
193
+
194
+ console.log(`\n✅ Export completed. Files saved to: ${baseDir}`);
195
+
196
+ // Print summary
197
+ console.log('\n📋 导出清单:');
198
+ console.log('='.repeat(80));
199
+ exportSummary.forEach((item, index) => {
200
+ console.log(`${index + 1}. ${item.name} (${item.id})`);
201
+ console.log(` 模板: ${item.templates.join(', ')}`);
202
+ });
203
+ console.log('='.repeat(80));
204
+ console.log(`共导出 ${exportSummary.length} 个流程`);
205
+
206
+ // Save summary to file
207
+ const summaryFile = path.join(baseDir, 'export-summary.json');
208
+ await fs.writeFile(
209
+ summaryFile,
210
+ JSON.stringify(exportSummary, null, 2),
211
+ 'utf8'
212
+ );
213
+ console.log(`\n清单已保存至: ${summaryFile}`);
214
+ } catch (error) {
215
+ console.error(`\n❌ Export failed: ${error.message}`);
216
+ throw error;
217
+ }
218
+ }
219
+
220
+ // Import templates from .liquid files
221
+ async function importTemplates(flowId, templateType, inputDir) {
222
+ try {
223
+ if (!flowId) {
224
+ throw new Error('flowId is required for import');
225
+ }
226
+
227
+ const baseDir = inputDir || path.join(process.cwd(), 'flows-template');
228
+ const flowsCollection = await getCollection('flows');
229
+
230
+ // Verify flow exists
231
+ const flow = await flowsCollection.findOne(
232
+ { _id: flowId },
233
+ { projection: { _id: 1, name: 1, instance_template: 1, print_template: 1 } }
234
+ );
235
+
236
+ if (!flow) {
237
+ throw new Error(`Flow not found: ${flowId}`);
238
+ }
239
+
240
+ console.log(`\nImporting templates for flow: ${flow.name} (${flow._id})`);
241
+
242
+ const updateObj = {
243
+ modified: new Date()
244
+ };
245
+ const imported = [];
246
+
247
+ // Determine which templates to import
248
+ const templatesToImport = templateType
249
+ ? [templateType]
250
+ : ['instance_template', 'print_template'];
251
+
252
+ for (const type of templatesToImport) {
253
+ const liquidFile = path.join(baseDir, `${flowId}.${type}.liquid`);
254
+
255
+ try {
256
+ // Check if file exists and is readable
257
+ await fs.access(liquidFile, fs.constants.R_OK);
258
+
259
+ // Read the liquid file
260
+ const liquidContent = await fs.readFile(liquidFile, 'utf8');
261
+
262
+ // Backup old value if it exists
263
+ const backupField = `${type}_backup`;
264
+ if (flow[type]) {
265
+ updateObj[backupField] = flow[type];
266
+ console.log(` ℹ Backing up old ${type} to ${backupField}`);
267
+ }
268
+
269
+ // Set new value
270
+ updateObj[type] = liquidContent;
271
+
272
+ imported.push({
273
+ type,
274
+ file: `${flowId}.${type}.liquid`,
275
+ backed_up: !!flow[type]
276
+ });
277
+
278
+ console.log(` ✓ Imported ${type} from ${flowId}.${type}.liquid`);
279
+ } catch (fileError) {
280
+ if (fileError.code === 'ENOENT') {
281
+ // File doesn't exist, skip
282
+ console.log(` ⊗ ${type} file not found, skipping`);
283
+ continue;
284
+ }
285
+ if (fileError.code === 'EACCES') {
286
+ throw new Error(`Permission denied reading file ${liquidFile}`);
287
+ }
288
+ throw new Error(`Failed to read file ${liquidFile}: ${fileError.message}`);
289
+ }
290
+ }
291
+
292
+ if (imported.length === 0) {
293
+ throw new Error(`No template files found for flow ${flowId}`);
294
+ }
295
+
296
+ // Update the flow
297
+ await flowsCollection.updateOne(
298
+ { _id: flowId },
299
+ { $set: updateObj }
300
+ );
301
+
302
+ console.log(`\n✅ Import completed. Imported ${imported.length} template(s).`);
303
+ imported.forEach(item => {
304
+ console.log(` - ${item.type}: ${item.backed_up ? 'updated (old value backed up)' : 'created'}`);
305
+ });
306
+ } catch (error) {
307
+ console.error(`\n❌ Import failed: ${error.message}`);
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ // Main function
313
+ async function main() {
314
+ const args = process.argv.slice(2);
315
+ const command = args[0];
316
+
317
+ if (!command || !['export', 'import'].includes(command)) {
318
+ console.log(`
319
+ Workflow Template Export/Import Script
320
+
321
+ Usage:
322
+ node run.js export [flowId] [outputDir]
323
+ node run.js import <flowId> [templateType] [inputDir]
324
+
325
+ Commands:
326
+ export Export templates to .blaze files
327
+ import Import templates from .liquid files
328
+
329
+ Export Examples:
330
+ node run.js export # Export all flows
331
+ node run.js export abc123 # Export specific flow
332
+ node run.js export abc123 /custom/path # Export to custom directory
333
+
334
+ Import Examples:
335
+ node run.js import abc123 # Import both templates
336
+ node run.js import abc123 instance_template # Import only instance_template
337
+ node run.js import abc123 print_template # Import only print_template
338
+
339
+ Directory Structure:
340
+ flows-template/
341
+ ├── {flowId}.instance_template.blaze # Exported Blaze template
342
+ ├── {flowId}.print_template.blaze # Exported Blaze template
343
+ ├── {flowId}.fields.json # Field definitions
344
+ ├── {flowId}.instance_template.liquid # Converted Liquid template (for import)
345
+ └── {flowId}.print_template.liquid # Converted Liquid template (for import)
346
+
347
+ Workflow:
348
+ 1. Export: node run.js export [flowId]
349
+ 2. Convert: Convert .blaze files to .liquid format (manually or with Copilot)
350
+ 3. Import: node run.js import <flowId>
351
+ `);
352
+ process.exit(command ? 1 : 0);
353
+ }
354
+
355
+ try {
356
+ if (command === 'export') {
357
+ const flowId = args[1];
358
+ const outputDir = args[2];
359
+ await exportTemplates(flowId, outputDir);
360
+ } else if (command === 'import') {
361
+ const flowId = args[1];
362
+ const templateType = args[2];
363
+ const inputDir = args[3];
364
+
365
+ // Validate templateType if provided
366
+ if (templateType && !['instance_template', 'print_template'].includes(templateType)) {
367
+ // Maybe it's the inputDir
368
+ await importTemplates(flowId, null, templateType);
369
+ } else {
370
+ await importTemplates(flowId, templateType, inputDir);
371
+ }
372
+ }
373
+
374
+ await closeMongoDB();
375
+ process.exit(0);
376
+ } catch (error) {
377
+ console.error('\n❌ Error:', error.message);
378
+ await closeMongoDB();
379
+ process.exit(1);
380
+ }
381
+ }
382
+
383
+ // Run if executed directly
384
+ if (require.main === module) {
385
+ main();
386
+ }
387
+
388
+ module.exports = { exportTemplates, importTemplates };
@@ -11,4 +11,5 @@ module.exports = {
11
11
  updateFormFields: require('./updateFormFields'),
12
12
  ...require('./getInstanceServiceSchema'),
13
13
  flow_copy: require('./flow_copy'),
14
+ migrateTemplates: require('./migrateTemplates'),
14
15
  }
@@ -0,0 +1,154 @@
1
+ const converter = require('../util/templateConverter');
2
+ const _ = require('lodash');
3
+ const { MongoClient } = require('mongodb');
4
+
5
+ // MongoDB connection
6
+ let client = null;
7
+ let db = null;
8
+
9
+ async function connectToMongoDB() {
10
+ if (db) {
11
+ return db;
12
+ }
13
+
14
+ const mongoUrl = process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/steedos';
15
+ try {
16
+ client = new MongoClient(mongoUrl);
17
+ await client.connect();
18
+
19
+ const dbName = mongoUrl.split('/').pop().split('?')[0];
20
+ db = client.db(dbName);
21
+ return db;
22
+ } catch (error) {
23
+ console.error('Failed to connect to MongoDB:', error.message);
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ async function getCollection(name) {
29
+ const database = await connectToMongoDB();
30
+ return database.collection(name);
31
+ }
32
+
33
+ module.exports = {
34
+ rest: {
35
+ method: 'GET',
36
+ fullPath: '/api/workflow/migrateTemplates'
37
+ },
38
+ params: {
39
+ batchSize: { type: 'number', optional: true, convert: true },
40
+ force: { type: 'boolean', optional: true, convert: true },
41
+ flowId: { type: 'string', optional: true }
42
+ },
43
+ async handler(ctx) {
44
+ const { user } = ctx.meta;
45
+
46
+ if (user.is_space_admin !== true) {
47
+ throw new Error('只有管理员才能执行此操作');
48
+ }
49
+
50
+ const { batchSize = 5000, force = false, flowId } = ctx.params;
51
+
52
+ const flowsCollection = await getCollection('flows');
53
+ const formsCollection = await getCollection('forms');
54
+
55
+ let flows = [];
56
+ if (flowId) {
57
+ flows = await flowsCollection.find({ _id: flowId }).toArray();
58
+ } else {
59
+ let query = {
60
+ $and: [
61
+ {
62
+ $or: [
63
+ { instance_template: { $exists: true, $ne: '' } },
64
+ { print_template: { $exists: true, $ne: '' } }
65
+ ]
66
+ }
67
+ ]
68
+ };
69
+
70
+ if (!force) {
71
+ query.$or = [
72
+ { migrate_status: { $ne: 'completed' } },
73
+ { migrate_status: { $exists: false } }
74
+ ];
75
+ }
76
+ flows = await flowsCollection.find(query).limit(batchSize).toArray();
77
+ }
78
+
79
+ const results = {
80
+ total: flows.length,
81
+ success: 0,
82
+ failed: 0,
83
+ skipped: 0,
84
+ details: []
85
+ };
86
+
87
+ for (const flow of flows) {
88
+ const flowId = flow._id;
89
+ const updateDoc = {};
90
+ let isChanged = false;
91
+
92
+ try {
93
+ let fields = {};
94
+ if (flow.form) {
95
+ const form = await formsCollection.findOne(
96
+ { _id: flow.form },
97
+ { projection: { 'current.fields': 1 } }
98
+ );
99
+ if (form && form.current && form.current.fields) {
100
+ fields = _.keyBy(form.current.fields, (f) => f.name || f.code);
101
+ fields._raw = form.current.fields;
102
+ }
103
+ }
104
+
105
+ if (!flow.backup_status) {
106
+ if (flow.instance_template) {
107
+ updateDoc.instance_template_backup = flow.instance_template;
108
+ }
109
+ if (flow.print_template) {
110
+ updateDoc.print_template_backup = flow.print_template;
111
+ }
112
+ updateDoc.backup_status = true;
113
+ isChanged = true;
114
+ }
115
+
116
+ const sourceInstance = updateDoc.instance_template_backup || flow.instance_template_backup || flow.instance_template;
117
+ const sourcePrint = updateDoc.print_template_backup || flow.print_template_backup || flow.print_template;
118
+
119
+ if (sourceInstance) {
120
+ const converted = converter.convertTemplate(sourceInstance, fields, false);
121
+ updateDoc.instance_template = converted;
122
+ isChanged = true;
123
+ }
124
+
125
+ if (sourcePrint) {
126
+ const converted = converter.convertTemplate(sourcePrint, fields, true);
127
+ updateDoc.print_template = converted;
128
+ isChanged = true;
129
+ }
130
+
131
+ updateDoc.migrate_status = 'completed';
132
+ updateDoc.migrated_at = new Date();
133
+
134
+ await flowsCollection.updateOne({ _id: flow._id }, { $set: updateDoc });
135
+
136
+ results.success++;
137
+ results.details.push({ id: flow._id, name: flow.name, status: 'success' });
138
+ } catch (error) {
139
+ console.error(`Error migrating flow ${flowId}:`, error);
140
+ results.failed++;
141
+ results.details.push({ id: flow._id, name: flow.name, status: 'error', error: error.message });
142
+
143
+ try {
144
+ await flowsCollection.updateOne(
145
+ { _id: flow._id },
146
+ { $set: { migrate_status: 'error', migrate_error: error.message } }
147
+ );
148
+ } catch (e) {}
149
+ }
150
+ }
151
+
152
+ return results;
153
+ }
154
+ }