@nocobase/plugin-action-import 2.1.0-beta.8 → 2.2.0-alpha.1
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/client-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/dist/client/ImportActionInitializer.d.ts +2 -6
- package/dist/client/buildImportFieldOptions.d.ts +9 -0
- package/dist/client/constants.d.ts +1 -1
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +1 -1
- package/dist/client/locale/index.d.ts +2 -1
- package/dist/client/useFields.d.ts +1 -1
- package/dist/{client/models → client-v2}/ImportActionModel.d.ts +2 -2
- package/dist/client-v2/buildImportFieldOptions.d.ts +9 -0
- package/dist/{client/models → client-v2}/getOptionFields.d.ts +1 -1
- package/dist/client-v2/importSupport.d.ts +15 -0
- package/dist/client-v2/index.d.ts +15 -0
- package/dist/client-v2/index.js +10 -0
- package/dist/client-v2/locale.d.ts +9 -0
- package/dist/externalVersion.js +11 -11
- package/dist/node_modules/exceljs/excel.js +15 -15
- package/dist/node_modules/exceljs/package.json +1 -1
- package/dist/node_modules/xlsx/dist/xlsx.core.min.js +17 -16
- package/dist/node_modules/xlsx/dist/xlsx.extendscript.js +378 -80
- package/dist/node_modules/xlsx/dist/xlsx.full.min.js +17 -17
- package/dist/node_modules/xlsx/dist/xlsx.mini.min.js +9 -9
- package/dist/node_modules/xlsx/package.json +1 -1
- package/dist/node_modules/xlsx/types/index.d.ts +40 -8
- package/dist/node_modules/xlsx/xlsx.js +4 -4
- package/dist/server/actions/import-xlsx.d.ts +3 -0
- package/dist/server/actions/import-xlsx.js +16 -8
- package/dist/server/services/xlsx-importer.d.ts +12 -4
- package/dist/server/services/xlsx-importer.js +82 -45
- package/package.json +4 -3
|
@@ -6,5 +6,8 @@
|
|
|
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
|
+
/// <reference types="node" />
|
|
9
10
|
import { Context, Next } from '@nocobase/actions';
|
|
11
|
+
import * as XLSX from 'xlsx';
|
|
12
|
+
export declare function readImportWorkbook(buffer: Buffer, sheetRows: number): XLSX.WorkBook;
|
|
10
13
|
export declare function importXlsx(ctx: Context, next: Next): Promise<void>;
|
|
@@ -36,14 +36,25 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
36
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
37
|
var import_xlsx_exports = {};
|
|
38
38
|
__export(import_xlsx_exports, {
|
|
39
|
-
importXlsx: () => importXlsx
|
|
39
|
+
importXlsx: () => importXlsx,
|
|
40
|
+
readImportWorkbook: () => readImportWorkbook
|
|
40
41
|
});
|
|
41
42
|
module.exports = __toCommonJS(import_xlsx_exports);
|
|
42
|
-
var
|
|
43
|
+
var XLSX = __toESM(require("xlsx"));
|
|
43
44
|
var import_async_mutex = require("async-mutex");
|
|
44
45
|
var import_xlsx_importer = require("../services/xlsx-importer");
|
|
45
46
|
const IMPORT_LIMIT_COUNT = 2e3;
|
|
46
47
|
const mutex = new import_async_mutex.Mutex();
|
|
48
|
+
function readImportWorkbook(buffer, sheetRows) {
|
|
49
|
+
return XLSX.read(buffer, {
|
|
50
|
+
type: "buffer",
|
|
51
|
+
sheetRows,
|
|
52
|
+
// Keep Excel date cells as serial numbers. SheetJS `cellDates: true` converts
|
|
53
|
+
// them to local-time Date objects through a timezone-sensitive path, which can
|
|
54
|
+
// shift date-only values in non-UTC environments (for example Asia/Shanghai).
|
|
55
|
+
cellDates: false
|
|
56
|
+
});
|
|
57
|
+
}
|
|
47
58
|
async function importXlsxAction(ctx, next) {
|
|
48
59
|
var _a;
|
|
49
60
|
let columns = ctx.request.body.columns;
|
|
@@ -55,11 +66,7 @@ async function importXlsxAction(ctx, next) {
|
|
|
55
66
|
if (ctx.request.body.explain) {
|
|
56
67
|
readLimit += 1;
|
|
57
68
|
}
|
|
58
|
-
const workbook =
|
|
59
|
-
type: "buffer",
|
|
60
|
-
sheetRows: readLimit,
|
|
61
|
-
cellDates: true
|
|
62
|
-
});
|
|
69
|
+
const workbook = readImportWorkbook(ctx.file.buffer, readLimit);
|
|
63
70
|
const repository = ctx.getCurrentRepository();
|
|
64
71
|
const dataSource = ctx.dataSource;
|
|
65
72
|
const collection = repository.collection;
|
|
@@ -96,5 +103,6 @@ async function importXlsx(ctx, next) {
|
|
|
96
103
|
}
|
|
97
104
|
// Annotate the CommonJS export names for ESM import in node:
|
|
98
105
|
0 && (module.exports = {
|
|
99
|
-
importXlsx
|
|
106
|
+
importXlsx,
|
|
107
|
+
readImportWorkbook
|
|
100
108
|
});
|
|
@@ -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,10 +55,10 @@ export declare class XlsxImporter extends EventEmitter {
|
|
|
47
55
|
run(options?: RunOptions): Promise<any>;
|
|
48
56
|
resetSeq(options?: RunOptions): Promise<void>;
|
|
49
57
|
private getColumnsByPermission;
|
|
50
|
-
|
|
58
|
+
protected validateColumns(ctx?: Context): void;
|
|
51
59
|
performImport(data: string[][], options?: RunOptions): Promise<any>;
|
|
52
60
|
protected getModel(): typeof Model;
|
|
53
|
-
handleRowValuesWithColumns(row: any, rowValues: any, options: RunOptions, columns: ImportColumn[]): Promise<void>;
|
|
61
|
+
handleRowValuesWithColumns(row: any, rowValues: any, options: RunOptions, columns: ImportColumn[], rowIndex?: number): Promise<void>;
|
|
54
62
|
handleChuckRows(chunkRows: string[][], runOptions?: RunOptions, options?: {
|
|
55
63
|
handingRowIndex: number;
|
|
56
64
|
context: any;
|
|
@@ -64,7 +72,7 @@ export declare class XlsxImporter extends EventEmitter {
|
|
|
64
72
|
associateRecords(targets: Model[], options?: any): Promise<void>;
|
|
65
73
|
renderErrorMessage(error: any): any;
|
|
66
74
|
trimString(str: string): string;
|
|
67
|
-
|
|
75
|
+
protected getExpectedHeaders(ctx?: Context): string[];
|
|
68
76
|
getData(ctx?: Context): Promise<string[][]>;
|
|
69
77
|
private alignWithHeaders;
|
|
70
78
|
private findAndValidateHeaders;
|
|
@@ -84,10 +84,33 @@ class XlsxImporter extends import_events.default {
|
|
|
84
84
|
return data;
|
|
85
85
|
}
|
|
86
86
|
async run(options = {}) {
|
|
87
|
-
var _a, _b;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|
|
91
114
|
}
|
|
92
115
|
try {
|
|
93
116
|
const data = await this.loggerService.measureExecutedTime(
|
|
@@ -98,18 +121,16 @@ class XlsxImporter extends import_events.default {
|
|
|
98
121
|
async () => this.performImport(data, options),
|
|
99
122
|
"Data import completed in {time}ms"
|
|
100
123
|
);
|
|
101
|
-
(
|
|
102
|
-
if (
|
|
124
|
+
(_c = this.logger) == null ? void 0 : _c.info(`Import completed successfully, imported ${imported} records`);
|
|
125
|
+
if (db) {
|
|
103
126
|
await this.loggerService.measureExecutedTime(
|
|
104
|
-
async () => this.resetSeq(
|
|
127
|
+
async () => this.resetSeq({}),
|
|
105
128
|
"Sequence reset completed in {time}ms"
|
|
106
129
|
);
|
|
107
130
|
}
|
|
108
|
-
transaction && await transaction.commit();
|
|
109
131
|
return imported;
|
|
110
132
|
} catch (error) {
|
|
111
|
-
|
|
112
|
-
(_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)}`, {
|
|
113
134
|
originalError: error.stack || error.toString()
|
|
114
135
|
});
|
|
115
136
|
throw error;
|
|
@@ -117,7 +138,7 @@ class XlsxImporter extends import_events.default {
|
|
|
117
138
|
}
|
|
118
139
|
async resetSeq(options) {
|
|
119
140
|
const { transaction } = options;
|
|
120
|
-
const db = this.options.collectionManager
|
|
141
|
+
const { db } = this.options.collectionManager;
|
|
121
142
|
const collection = this.options.collection;
|
|
122
143
|
const autoIncrementAttribute = collection.model.autoIncrementAttribute;
|
|
123
144
|
if (!autoIncrementAttribute) {
|
|
@@ -166,13 +187,13 @@ class XlsxImporter extends import_events.default {
|
|
|
166
187
|
this.emit("seqReset", { maxVal, seqName: autoIncrInfo.seqName });
|
|
167
188
|
}
|
|
168
189
|
getColumnsByPermission(ctx) {
|
|
190
|
+
var _a, _b, _c;
|
|
169
191
|
const columns = this.options.columns;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
);
|
|
192
|
+
const fields = (_c = (_b = (_a = ctx == null ? void 0 : ctx.permission) == null ? void 0 : _a.can) == null ? void 0 : _b.params) == null ? void 0 : _c.fields;
|
|
193
|
+
if (!Array.isArray(fields)) {
|
|
194
|
+
return columns;
|
|
195
|
+
}
|
|
196
|
+
return columns.filter((x) => import_lodash2.default.includes(fields, x.dataIndex[0]));
|
|
176
197
|
}
|
|
177
198
|
validateColumns(ctx) {
|
|
178
199
|
var _a, _b, _c;
|
|
@@ -233,8 +254,10 @@ class XlsxImporter extends import_events.default {
|
|
|
233
254
|
if (this.options.explain) {
|
|
234
255
|
handingRowIndex += 1;
|
|
235
256
|
}
|
|
236
|
-
|
|
257
|
+
let chunkRows;
|
|
258
|
+
while ((chunkRows = chunks.shift()) !== void 0) {
|
|
237
259
|
await this.handleChuckRows(chunkRows, options, { handingRowIndex, context: options == null ? void 0 : options.context });
|
|
260
|
+
handingRowIndex += chunkRows.length;
|
|
238
261
|
imported += chunkRows.length;
|
|
239
262
|
this.emit("progress", {
|
|
240
263
|
total,
|
|
@@ -246,8 +269,7 @@ class XlsxImporter extends import_events.default {
|
|
|
246
269
|
getModel() {
|
|
247
270
|
return this.repository instanceof import_database.RelationRepository ? this.repository.targetModel : this.repository.model;
|
|
248
271
|
}
|
|
249
|
-
async handleRowValuesWithColumns(row, rowValues, options, columns) {
|
|
250
|
-
var _a;
|
|
272
|
+
async handleRowValuesWithColumns(row, rowValues, options, columns, rowIndex = 1) {
|
|
251
273
|
for (let index = 0; index < columns.length; index++) {
|
|
252
274
|
const column = columns[index];
|
|
253
275
|
const field = this.options.collection.getField(column.dataIndex[0]);
|
|
@@ -266,10 +288,9 @@ class XlsxImporter extends import_events.default {
|
|
|
266
288
|
continue;
|
|
267
289
|
}
|
|
268
290
|
const interfaceInstance = new InterfaceClass(field.options);
|
|
269
|
-
const ctx = {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
};
|
|
291
|
+
const ctx = options.context ? Object.create(options.context) : {};
|
|
292
|
+
ctx.transaction = options.transaction;
|
|
293
|
+
ctx.field = field;
|
|
273
294
|
if (column.dataIndex.length > 1) {
|
|
274
295
|
ctx.associationField = field;
|
|
275
296
|
ctx.targetCollection = field.targetCollection();
|
|
@@ -279,7 +300,7 @@ class XlsxImporter extends import_events.default {
|
|
|
279
300
|
rowValues[dataKey] = str == null ? null : await interfaceInstance.toValue(this.trimString(str), ctx);
|
|
280
301
|
} catch (error) {
|
|
281
302
|
throw new import_errors.ImportValidationError("Failed to parse field {{field}} in row {{rowIndex}}: {{message}}", {
|
|
282
|
-
rowIndex
|
|
303
|
+
rowIndex,
|
|
283
304
|
field: dataKey,
|
|
284
305
|
message: error.message
|
|
285
306
|
});
|
|
@@ -295,18 +316,12 @@ class XlsxImporter extends import_events.default {
|
|
|
295
316
|
}
|
|
296
317
|
async handleChuckRows(chunkRows, runOptions, options) {
|
|
297
318
|
var _a, _b;
|
|
298
|
-
|
|
299
|
-
const
|
|
319
|
+
const { handingRowIndex: chunkStartRowIndex = 1 } = options;
|
|
320
|
+
const db = this.options.collectionManager.db;
|
|
321
|
+
const externalTransaction = runOptions == null ? void 0 : runOptions.transaction;
|
|
322
|
+
const transaction = externalTransaction || (db ? await db.sequelize.transaction() : null);
|
|
323
|
+
const chunkRunOptions = { ...runOptions, transaction };
|
|
300
324
|
const columns = this.getColumnsByPermission(options == null ? void 0 : options.context);
|
|
301
|
-
const rows = [];
|
|
302
|
-
for (const row of chunkRows) {
|
|
303
|
-
const rowValues = {};
|
|
304
|
-
await this.handleRowValuesWithColumns(row, rowValues, runOptions, columns);
|
|
305
|
-
rows.push({
|
|
306
|
-
...this.options.rowDefaultValues || {},
|
|
307
|
-
...rowValues
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
325
|
const translate = (message) => {
|
|
311
326
|
var _a2;
|
|
312
327
|
if ((_a2 = options.context) == null ? void 0 : _a2.t) {
|
|
@@ -315,7 +330,19 @@ class XlsxImporter extends import_events.default {
|
|
|
315
330
|
return message;
|
|
316
331
|
}
|
|
317
332
|
};
|
|
333
|
+
const rows = [];
|
|
334
|
+
let inChunkRowIndex = 0;
|
|
318
335
|
try {
|
|
336
|
+
for (let i = 0; i < chunkRows.length; i++) {
|
|
337
|
+
inChunkRowIndex = i;
|
|
338
|
+
const row = chunkRows[i];
|
|
339
|
+
const rowValues = {};
|
|
340
|
+
await this.handleRowValuesWithColumns(row, rowValues, chunkRunOptions, columns, chunkStartRowIndex + i);
|
|
341
|
+
rows.push({
|
|
342
|
+
...this.options.rowDefaultValues || {},
|
|
343
|
+
...rowValues
|
|
344
|
+
});
|
|
345
|
+
}
|
|
319
346
|
await this.loggerService.measureExecutedTime(
|
|
320
347
|
async () => this.performInsert({
|
|
321
348
|
values: rows,
|
|
@@ -325,23 +352,31 @@ class XlsxImporter extends import_events.default {
|
|
|
325
352
|
"Record insertion completed in {time}ms"
|
|
326
353
|
);
|
|
327
354
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
328
|
-
|
|
355
|
+
if (!externalTransaction) {
|
|
356
|
+
await (transaction == null ? void 0 : transaction.commit());
|
|
357
|
+
}
|
|
329
358
|
} catch (error) {
|
|
359
|
+
if (!externalTransaction) {
|
|
360
|
+
await (transaction == null ? void 0 : transaction.rollback());
|
|
361
|
+
}
|
|
362
|
+
if (error.name === "ImportValidationError") {
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
330
365
|
if (error.name === "SequelizeUniqueConstraintError") {
|
|
331
366
|
throw new Error(`${translate("Unique constraint error, fields:")} ${JSON.stringify(error.fields)}`);
|
|
332
367
|
}
|
|
368
|
+
const failedRowIndex = chunkStartRowIndex + inChunkRowIndex;
|
|
333
369
|
if ((_a = error.params) == null ? void 0 : _a.rowIndex) {
|
|
334
|
-
|
|
335
|
-
error.params.rowIndex = handingRowIndex;
|
|
370
|
+
error.params.rowIndex = failedRowIndex;
|
|
336
371
|
}
|
|
337
|
-
(_b = this.logger) == null ? void 0 : _b.error(`Import error at row ${
|
|
338
|
-
rowIndex:
|
|
339
|
-
rowData:
|
|
372
|
+
(_b = this.logger) == null ? void 0 : _b.error(`Import error at row ${failedRowIndex}: ${error.message}`, {
|
|
373
|
+
rowIndex: failedRowIndex,
|
|
374
|
+
rowData: chunkRows[inChunkRowIndex],
|
|
340
375
|
originalError: error.stack || error.toString()
|
|
341
376
|
});
|
|
342
|
-
throw new import_errors.ImportError(`Import failed at row ${
|
|
343
|
-
rowIndex:
|
|
344
|
-
rowData:
|
|
377
|
+
throw new import_errors.ImportError(`Import failed at row ${failedRowIndex}`, {
|
|
378
|
+
rowIndex: failedRowIndex,
|
|
379
|
+
rowData: chunkRows[inChunkRowIndex],
|
|
345
380
|
cause: error
|
|
346
381
|
});
|
|
347
382
|
}
|
|
@@ -400,6 +435,7 @@ class XlsxImporter extends import_events.default {
|
|
|
400
435
|
"debug"
|
|
401
436
|
);
|
|
402
437
|
}
|
|
438
|
+
instances[i] = null;
|
|
403
439
|
}
|
|
404
440
|
return instances;
|
|
405
441
|
}
|
|
@@ -448,6 +484,7 @@ class XlsxImporter extends import_events.default {
|
|
|
448
484
|
const workbook = this.options.workbook;
|
|
449
485
|
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
450
486
|
let data = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: null, blankrows: false });
|
|
487
|
+
this.options.workbook = null;
|
|
451
488
|
const expectedHeaders = this.getExpectedHeaders(ctx);
|
|
452
489
|
const { headerRowIndex, headers } = this.findAndValidateHeaders({ data, expectedHeaders });
|
|
453
490
|
if (headerRowIndex === -1) {
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
"version": "2.2.0-alpha.1",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"main": "./dist/server/index.js",
|
|
12
12
|
"homepage": "https://docs.nocobase.com/handbook/action-import",
|
|
@@ -36,17 +36,18 @@
|
|
|
36
36
|
"react": "^18.2.0",
|
|
37
37
|
"react-dom": "^18.2.0",
|
|
38
38
|
"react-i18next": "^11.15.1",
|
|
39
|
-
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.
|
|
39
|
+
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@nocobase/actions": "2.x",
|
|
43
43
|
"@nocobase/client": "2.x",
|
|
44
|
+
"@nocobase/client-v2": "2.x",
|
|
44
45
|
"@nocobase/database": "2.x",
|
|
45
46
|
"@nocobase/server": "2.x",
|
|
46
47
|
"@nocobase/test": "2.x",
|
|
47
48
|
"@nocobase/utils": "2.x"
|
|
48
49
|
},
|
|
49
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "303663aba6c6eefa27e6a6435b4c0352074ec40f",
|
|
50
51
|
"keywords": [
|
|
51
52
|
"Actions"
|
|
52
53
|
]
|