@nocobase/plugin-action-import 2.1.0-alpha.1 → 2.1.0-alpha.11
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/LICENSE +201 -661
- package/README.md +79 -10
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +9 -9
- package/dist/node_modules/exceljs/package.json +1 -1
- package/dist/node_modules/xlsx/package.json +1 -1
- package/dist/server/flow-schema-contributions/index.d.ts +10 -0
- package/dist/server/flow-schema-contributions/index.js +122 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/services/xlsx-importer.d.ts +11 -2
- package/dist/server/services/xlsx-importer.js +126 -39
- package/package.json +11 -4
package/dist/server/index.d.ts
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
+
import type { FlowSchemaContribution } from '@nocobase/flow-engine';
|
|
9
10
|
import { Plugin } from '@nocobase/server';
|
|
10
11
|
export { ImportError, ImportValidationError } from './errors';
|
|
11
12
|
export declare class PluginActionImportServer extends Plugin {
|
|
13
|
+
getFlowSchemaContributions(): FlowSchemaContribution;
|
|
12
14
|
beforeLoad(): void;
|
|
13
15
|
load(): Promise<void>;
|
|
14
16
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -37,10 +37,14 @@ var import_server = require("@nocobase/server");
|
|
|
37
37
|
var import_actions = require("./actions");
|
|
38
38
|
var import_middleware = require("./middleware");
|
|
39
39
|
var import_errors = require("./errors");
|
|
40
|
+
var import_flow_schema_contributions = require("./flow-schema-contributions");
|
|
40
41
|
var import_errors2 = require("./errors");
|
|
41
42
|
__reExport(server_exports, require("./services/xlsx-importer"), module.exports);
|
|
42
43
|
__reExport(server_exports, require("./services/template-creator"), module.exports);
|
|
43
44
|
class PluginActionImportServer extends import_server.Plugin {
|
|
45
|
+
getFlowSchemaContributions() {
|
|
46
|
+
return import_flow_schema_contributions.flowSchemaContribution;
|
|
47
|
+
}
|
|
44
48
|
beforeLoad() {
|
|
45
49
|
this.app.on("afterInstall", async () => {
|
|
46
50
|
if (!this.app.db.getRepository("roles")) {
|
|
@@ -24,7 +24,15 @@ export type ImporterOptions = {
|
|
|
24
24
|
collectionManager: ICollectionManager;
|
|
25
25
|
collection: ICollection;
|
|
26
26
|
columns: Array<ImportColumn>;
|
|
27
|
-
workbook
|
|
27
|
+
/** Parsed SheetJS workbook. Used for the synchronous (small-file) import path. */
|
|
28
|
+
workbook?: any;
|
|
29
|
+
/**
|
|
30
|
+
* Absolute path to the XLSX file on disk. When set, the importer uses ExcelJS
|
|
31
|
+
* streaming reader so the file is never fully loaded into memory — rows are
|
|
32
|
+
* yielded one by one and processed in chunks. This is the preferred path for
|
|
33
|
+
* large async imports.
|
|
34
|
+
*/
|
|
35
|
+
filePath?: string;
|
|
28
36
|
chunkSize?: number;
|
|
29
37
|
explain?: string;
|
|
30
38
|
repository?: any;
|
|
@@ -47,6 +55,7 @@ export declare class XlsxImporter extends EventEmitter {
|
|
|
47
55
|
run(options?: RunOptions): Promise<any>;
|
|
48
56
|
resetSeq(options?: RunOptions): Promise<void>;
|
|
49
57
|
private getColumnsByPermission;
|
|
58
|
+
protected validateColumns(ctx?: Context): void;
|
|
50
59
|
performImport(data: string[][], options?: RunOptions): Promise<any>;
|
|
51
60
|
protected getModel(): typeof Model;
|
|
52
61
|
handleRowValuesWithColumns(row: any, rowValues: any, options: RunOptions, columns: ImportColumn[]): Promise<void>;
|
|
@@ -63,7 +72,7 @@ export declare class XlsxImporter extends EventEmitter {
|
|
|
63
72
|
associateRecords(targets: Model[], options?: any): Promise<void>;
|
|
64
73
|
renderErrorMessage(error: any): any;
|
|
65
74
|
trimString(str: string): string;
|
|
66
|
-
|
|
75
|
+
protected getExpectedHeaders(ctx?: Context): string[];
|
|
67
76
|
getData(ctx?: Context): Promise<string[][]>;
|
|
68
77
|
private alignWithHeaders;
|
|
69
78
|
private findAndValidateHeaders;
|
|
@@ -78,25 +78,39 @@ class XlsxImporter extends import_events.default {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
async validate(ctx) {
|
|
81
|
-
|
|
82
|
-
if (columns.length == 0) {
|
|
83
|
-
throw new import_errors.ImportValidationError("Columns configuration is empty");
|
|
84
|
-
}
|
|
85
|
-
for (const column of this.options.columns) {
|
|
86
|
-
const field = this.options.collection.getField(column.dataIndex[0]);
|
|
87
|
-
if (!field) {
|
|
88
|
-
throw new import_errors.ImportValidationError("Field not found: {{field}}", { field: column.dataIndex[0] });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
81
|
+
this.validateColumns(ctx);
|
|
91
82
|
const data = await this.getData(ctx);
|
|
92
83
|
await this.validateBySpaces(data, ctx);
|
|
93
84
|
return data;
|
|
94
85
|
}
|
|
95
86
|
async run(options = {}) {
|
|
96
|
-
var _a, _b;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
var _a, _b, _c, _d;
|
|
88
|
+
const hasExternalTransaction = !!options.transaction;
|
|
89
|
+
const { db } = this.options.collectionManager;
|
|
90
|
+
if (hasExternalTransaction) {
|
|
91
|
+
try {
|
|
92
|
+
const data = await this.loggerService.measureExecutedTime(
|
|
93
|
+
async () => this.validate(options.context),
|
|
94
|
+
"Validation completed in {time}ms"
|
|
95
|
+
);
|
|
96
|
+
const imported = await this.loggerService.measureExecutedTime(
|
|
97
|
+
async () => this.performImport(data, options),
|
|
98
|
+
"Data import completed in {time}ms"
|
|
99
|
+
);
|
|
100
|
+
(_a = this.logger) == null ? void 0 : _a.info(`Import completed successfully, imported ${imported} records`);
|
|
101
|
+
if (db) {
|
|
102
|
+
await this.loggerService.measureExecutedTime(
|
|
103
|
+
async () => this.resetSeq(options),
|
|
104
|
+
"Sequence reset completed in {time}ms"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return imported;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
(_b = this.logger) == null ? void 0 : _b.error(`Import failed: ${this.renderErrorMessage(error)}`, {
|
|
110
|
+
originalError: error.stack || error.toString()
|
|
111
|
+
});
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
100
114
|
}
|
|
101
115
|
try {
|
|
102
116
|
const data = await this.loggerService.measureExecutedTime(
|
|
@@ -107,18 +121,16 @@ class XlsxImporter extends import_events.default {
|
|
|
107
121
|
async () => this.performImport(data, options),
|
|
108
122
|
"Data import completed in {time}ms"
|
|
109
123
|
);
|
|
110
|
-
(
|
|
111
|
-
if (
|
|
124
|
+
(_c = this.logger) == null ? void 0 : _c.info(`Import completed successfully, imported ${imported} records`);
|
|
125
|
+
if (db) {
|
|
112
126
|
await this.loggerService.measureExecutedTime(
|
|
113
|
-
async () => this.resetSeq(
|
|
127
|
+
async () => this.resetSeq({}),
|
|
114
128
|
"Sequence reset completed in {time}ms"
|
|
115
129
|
);
|
|
116
130
|
}
|
|
117
|
-
transaction && await transaction.commit();
|
|
118
131
|
return imported;
|
|
119
132
|
} catch (error) {
|
|
120
|
-
|
|
121
|
-
(_b = this.logger) == null ? void 0 : _b.error(`Import failed: ${this.renderErrorMessage(error)}`, {
|
|
133
|
+
(_d = this.logger) == null ? void 0 : _d.error(`Import failed: ${this.renderErrorMessage(error)}`, {
|
|
122
134
|
originalError: error.stack || error.toString()
|
|
123
135
|
});
|
|
124
136
|
throw error;
|
|
@@ -126,7 +138,7 @@ class XlsxImporter extends import_events.default {
|
|
|
126
138
|
}
|
|
127
139
|
async resetSeq(options) {
|
|
128
140
|
const { transaction } = options;
|
|
129
|
-
const db = this.options.collectionManager
|
|
141
|
+
const { db } = this.options.collectionManager;
|
|
130
142
|
const collection = this.options.collection;
|
|
131
143
|
const autoIncrementAttribute = collection.model.autoIncrementAttribute;
|
|
132
144
|
if (!autoIncrementAttribute) {
|
|
@@ -158,6 +170,12 @@ class XlsxImporter extends import_events.default {
|
|
|
158
170
|
transaction
|
|
159
171
|
});
|
|
160
172
|
const maxVal = await collection.model.max(autoIncrementAttribute, { transaction });
|
|
173
|
+
if (maxVal == null) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (typeof autoIncrInfo.currentVal === "number" && maxVal <= autoIncrInfo.currentVal) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
161
179
|
const queryInterface = db.queryInterface;
|
|
162
180
|
await queryInterface.setAutoIncrementVal({
|
|
163
181
|
tableInfo,
|
|
@@ -177,6 +195,56 @@ class XlsxImporter extends import_events.default {
|
|
|
177
195
|
}
|
|
178
196
|
);
|
|
179
197
|
}
|
|
198
|
+
validateColumns(ctx) {
|
|
199
|
+
var _a, _b, _c;
|
|
200
|
+
const columns = this.getColumnsByPermission(ctx);
|
|
201
|
+
if (columns.length === 0) {
|
|
202
|
+
throw new import_errors.ImportValidationError("Columns configuration is empty");
|
|
203
|
+
}
|
|
204
|
+
for (const column of columns) {
|
|
205
|
+
if (!Array.isArray(column == null ? void 0 : column.dataIndex) || column.dataIndex.length === 0) {
|
|
206
|
+
throw new import_errors.ImportValidationError("Columns configuration is empty");
|
|
207
|
+
}
|
|
208
|
+
if (column.dataIndex.length > 2) {
|
|
209
|
+
throw new import_errors.ImportValidationError("Invalid field: {{field}}", {
|
|
210
|
+
field: column.dataIndex.join(".")
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const [fieldName, filterKey] = column.dataIndex;
|
|
214
|
+
if (typeof fieldName !== "string" || fieldName.trim() === "") {
|
|
215
|
+
throw new import_errors.ImportValidationError("Invalid field: {{field}}", { field: String(fieldName) });
|
|
216
|
+
}
|
|
217
|
+
const field = this.options.collection.getField(fieldName);
|
|
218
|
+
if (!field) {
|
|
219
|
+
throw new import_errors.ImportValidationError("Field not found: {{field}}", { field: fieldName });
|
|
220
|
+
}
|
|
221
|
+
if (column.dataIndex.length > 1) {
|
|
222
|
+
if (typeof field.isRelationField !== "function" || !field.isRelationField()) {
|
|
223
|
+
throw new import_errors.ImportValidationError("Invalid field: {{field}}", {
|
|
224
|
+
field: column.dataIndex.join(".")
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (typeof filterKey !== "string" || filterKey.trim() === "") {
|
|
228
|
+
throw new import_errors.ImportValidationError("Invalid field: {{field}}", {
|
|
229
|
+
field: column.dataIndex.join(".")
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const targetCollection = (_a = field.targetCollection) == null ? void 0 : _a.call(field);
|
|
233
|
+
if (!targetCollection) {
|
|
234
|
+
throw new import_errors.ImportValidationError("Field not found: {{field}}", {
|
|
235
|
+
field: column.dataIndex.join(".")
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const targetField = targetCollection.getField(filterKey);
|
|
239
|
+
const isValidAttribute = (_c = (_b = targetCollection.model) == null ? void 0 : _b.getAttributes()) == null ? void 0 : _c[filterKey];
|
|
240
|
+
if (!targetField && !isValidAttribute) {
|
|
241
|
+
throw new import_errors.ImportValidationError("Field not found: {{field}}", {
|
|
242
|
+
field: `${fieldName}.${filterKey}`
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
180
248
|
async performImport(data, options) {
|
|
181
249
|
const chunkSize = this.options.chunkSize || 1e3;
|
|
182
250
|
const chunks = import_lodash.default.chunk(data.slice(1), chunkSize);
|
|
@@ -186,7 +254,8 @@ class XlsxImporter extends import_events.default {
|
|
|
186
254
|
if (this.options.explain) {
|
|
187
255
|
handingRowIndex += 1;
|
|
188
256
|
}
|
|
189
|
-
|
|
257
|
+
let chunkRows;
|
|
258
|
+
while ((chunkRows = chunks.shift()) !== void 0) {
|
|
190
259
|
await this.handleChuckRows(chunkRows, options, { handingRowIndex, context: options == null ? void 0 : options.context });
|
|
191
260
|
imported += chunkRows.length;
|
|
192
261
|
this.emit("progress", {
|
|
@@ -247,20 +316,31 @@ class XlsxImporter extends import_events.default {
|
|
|
247
316
|
rowValues = this.repository instanceof import_database.RelationRepository ? model.callSetters(guard.sanitize(rowValues || {}), options) : guard.sanitize(rowValues);
|
|
248
317
|
}
|
|
249
318
|
async handleChuckRows(chunkRows, runOptions, options) {
|
|
250
|
-
var _a, _b
|
|
319
|
+
var _a, _b;
|
|
251
320
|
let { handingRowIndex = 1 } = options;
|
|
252
|
-
const
|
|
321
|
+
const db = this.options.collectionManager.db;
|
|
322
|
+
const externalTransaction = runOptions == null ? void 0 : runOptions.transaction;
|
|
323
|
+
const transaction = externalTransaction || (db ? await db.sequelize.transaction() : null);
|
|
324
|
+
const chunkRunOptions = { ...runOptions, transaction };
|
|
253
325
|
const columns = this.getColumnsByPermission(options == null ? void 0 : options.context);
|
|
326
|
+
const translate = (message) => {
|
|
327
|
+
var _a2;
|
|
328
|
+
if ((_a2 = options.context) == null ? void 0 : _a2.t) {
|
|
329
|
+
return options.context.t(message, { ns: "action-import" });
|
|
330
|
+
} else {
|
|
331
|
+
return message;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
254
334
|
const rows = [];
|
|
255
|
-
for (const row of chunkRows) {
|
|
256
|
-
const rowValues = {};
|
|
257
|
-
await this.handleRowValuesWithColumns(row, rowValues, runOptions, columns);
|
|
258
|
-
rows.push({
|
|
259
|
-
...this.options.rowDefaultValues || {},
|
|
260
|
-
...rowValues
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
335
|
try {
|
|
336
|
+
for (const row of chunkRows) {
|
|
337
|
+
const rowValues = {};
|
|
338
|
+
await this.handleRowValuesWithColumns(row, rowValues, chunkRunOptions, columns);
|
|
339
|
+
rows.push({
|
|
340
|
+
...this.options.rowDefaultValues || {},
|
|
341
|
+
...rowValues
|
|
342
|
+
});
|
|
343
|
+
}
|
|
264
344
|
await this.loggerService.measureExecutedTime(
|
|
265
345
|
async () => this.performInsert({
|
|
266
346
|
values: rows,
|
|
@@ -271,19 +351,24 @@ class XlsxImporter extends import_events.default {
|
|
|
271
351
|
);
|
|
272
352
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
273
353
|
handingRowIndex += chunkRows.length;
|
|
354
|
+
if (!externalTransaction) {
|
|
355
|
+
await (transaction == null ? void 0 : transaction.commit());
|
|
356
|
+
}
|
|
274
357
|
} catch (error) {
|
|
358
|
+
if (!externalTransaction) {
|
|
359
|
+
await (transaction == null ? void 0 : transaction.rollback());
|
|
360
|
+
}
|
|
361
|
+
if (error.name === "ImportValidationError") {
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
275
364
|
if (error.name === "SequelizeUniqueConstraintError") {
|
|
276
|
-
throw new Error(
|
|
277
|
-
`${(_a = options.context) == null ? void 0 : _a.t("Unique constraint error, fields:", { ns: "action-import" })} ${JSON.stringify(
|
|
278
|
-
error.fields
|
|
279
|
-
)}`
|
|
280
|
-
);
|
|
365
|
+
throw new Error(`${translate("Unique constraint error, fields:")} ${JSON.stringify(error.fields)}`);
|
|
281
366
|
}
|
|
282
|
-
if ((
|
|
367
|
+
if ((_a = error.params) == null ? void 0 : _a.rowIndex) {
|
|
283
368
|
handingRowIndex += error.params.rowIndex;
|
|
284
369
|
error.params.rowIndex = handingRowIndex;
|
|
285
370
|
}
|
|
286
|
-
(
|
|
371
|
+
(_b = this.logger) == null ? void 0 : _b.error(`Import error at row ${handingRowIndex}: ${error.message}`, {
|
|
287
372
|
rowIndex: handingRowIndex,
|
|
288
373
|
rowData: rows[handingRowIndex],
|
|
289
374
|
originalError: error.stack || error.toString()
|
|
@@ -349,6 +434,7 @@ class XlsxImporter extends import_events.default {
|
|
|
349
434
|
"debug"
|
|
350
435
|
);
|
|
351
436
|
}
|
|
437
|
+
instances[i] = null;
|
|
352
438
|
}
|
|
353
439
|
return instances;
|
|
354
440
|
}
|
|
@@ -397,6 +483,7 @@ class XlsxImporter extends import_events.default {
|
|
|
397
483
|
const workbook = this.options.workbook;
|
|
398
484
|
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
399
485
|
let data = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: null, blankrows: false });
|
|
486
|
+
this.options.workbook = null;
|
|
400
487
|
const expectedHeaders = this.getExpectedHeaders(ctx);
|
|
401
488
|
const { headerRowIndex, headers } = this.findAndValidateHeaders({ data, expectedHeaders });
|
|
402
489
|
if (headerRowIndex === -1) {
|
package/package.json
CHANGED
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
"description": "Import records using excel templates. You can configure which fields to import and templates will be generated automatically.",
|
|
7
7
|
"description.ru-RU": "Импорт записей с помощью шаблонов Excel: можно настроить, какие поля импортировать, шаблоны будут генерироваться автоматически.",
|
|
8
8
|
"description.zh-CN": "使用 Excel 模板导入数据,可以配置导入哪些字段,自动生成模板。",
|
|
9
|
-
"version": "2.1.0-alpha.
|
|
10
|
-
"license": "
|
|
9
|
+
"version": "2.1.0-alpha.11",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
11
|
"main": "./dist/server/index.js",
|
|
12
12
|
"homepage": "https://docs.nocobase.com/handbook/action-import",
|
|
13
13
|
"homepage.ru-RU": "https://docs.nocobase.ru/handbook/action-import",
|
|
14
14
|
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-import",
|
|
15
|
+
"nocobase": {
|
|
16
|
+
"supportedVersions": [
|
|
17
|
+
"1.x",
|
|
18
|
+
"2.x"
|
|
19
|
+
],
|
|
20
|
+
"editionLevel": 0
|
|
21
|
+
},
|
|
15
22
|
"devDependencies": {
|
|
16
23
|
"@ant-design/icons": "5.x",
|
|
17
24
|
"@formily/antd-v5": "1.x",
|
|
@@ -24,7 +31,7 @@
|
|
|
24
31
|
"async-mutex": "^0.5.0",
|
|
25
32
|
"exceljs": "^4.4.0",
|
|
26
33
|
"file-saver": "^2.0.5",
|
|
27
|
-
"mathjs": "^
|
|
34
|
+
"mathjs": "^15.1.0",
|
|
28
35
|
"node-xlsx": "^0.16.1",
|
|
29
36
|
"react": "^18.2.0",
|
|
30
37
|
"react-dom": "^18.2.0",
|
|
@@ -39,7 +46,7 @@
|
|
|
39
46
|
"@nocobase/test": "2.x",
|
|
40
47
|
"@nocobase/utils": "2.x"
|
|
41
48
|
},
|
|
42
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "bb96d633a6371afb586072ff516bd0613c757db0",
|
|
43
50
|
"keywords": [
|
|
44
51
|
"Actions"
|
|
45
52
|
]
|