@nocobase/plugin-action-import 2.1.0-beta.9 → 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.
Files changed (31) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/ImportActionInitializer.d.ts +2 -6
  4. package/dist/client/buildImportFieldOptions.d.ts +9 -0
  5. package/dist/client/constants.d.ts +1 -1
  6. package/dist/client/index.d.ts +2 -0
  7. package/dist/client/index.js +1 -1
  8. package/dist/client/locale/index.d.ts +2 -1
  9. package/dist/client/useFields.d.ts +1 -1
  10. package/dist/{client/models → client-v2}/ImportActionModel.d.ts +2 -2
  11. package/dist/client-v2/buildImportFieldOptions.d.ts +9 -0
  12. package/dist/{client/models → client-v2}/getOptionFields.d.ts +1 -1
  13. package/dist/client-v2/importSupport.d.ts +15 -0
  14. package/dist/client-v2/index.d.ts +15 -0
  15. package/dist/client-v2/index.js +10 -0
  16. package/dist/client-v2/locale.d.ts +9 -0
  17. package/dist/externalVersion.js +11 -11
  18. package/dist/node_modules/exceljs/excel.js +15 -15
  19. package/dist/node_modules/exceljs/package.json +1 -1
  20. package/dist/node_modules/xlsx/dist/xlsx.core.min.js +17 -16
  21. package/dist/node_modules/xlsx/dist/xlsx.extendscript.js +378 -80
  22. package/dist/node_modules/xlsx/dist/xlsx.full.min.js +17 -17
  23. package/dist/node_modules/xlsx/dist/xlsx.mini.min.js +9 -9
  24. package/dist/node_modules/xlsx/package.json +1 -1
  25. package/dist/node_modules/xlsx/types/index.d.ts +40 -8
  26. package/dist/node_modules/xlsx/xlsx.js +4 -4
  27. package/dist/server/actions/import-xlsx.d.ts +3 -0
  28. package/dist/server/actions/import-xlsx.js +16 -8
  29. package/dist/server/services/xlsx-importer.d.ts +12 -4
  30. package/dist/server/services/xlsx-importer.js +82 -45
  31. 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 import_xlsx = __toESM(require("xlsx"));
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 = import_xlsx.default.read(ctx.file.buffer, {
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: any;
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
- private validateColumns;
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
- private getExpectedHeaders;
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
- let transaction = options.transaction;
89
- if (!transaction && this.options.collectionManager.db) {
90
- transaction = options.transaction = await this.options.collectionManager.db.sequelize.transaction();
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
- (_a = this.logger) == null ? void 0 : _a.info(`Import completed successfully, imported ${imported} records`);
102
- if (this.options.collectionManager.db) {
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(options),
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
- transaction && await transaction.rollback();
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.db;
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
- return columns.filter(
171
- (x) => {
172
- var _a, _b, _c, _d, _e;
173
- return import_lodash2.default.isEmpty((_b = (_a = ctx == null ? void 0 : ctx.permission) == null ? void 0 : _a.can) == null ? void 0 : _b.params) ? true : import_lodash2.default.includes(((_e = (_d = (_c = ctx == null ? void 0 : ctx.permission) == null ? void 0 : _c.can) == null ? void 0 : _d.params) == null ? void 0 : _e.fields) || [], x.dataIndex[0]);
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
- for (const chunkRows of chunks) {
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
- transaction: options.transaction,
271
- field
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: ((_a = options == null ? void 0 : options.context) == null ? void 0 : _a.handingRowIndex) || 1,
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
- let { handingRowIndex = 1 } = options;
299
- const { transaction } = runOptions;
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
- handingRowIndex += chunkRows.length;
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
- handingRowIndex += error.params.rowIndex;
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 ${handingRowIndex}: ${error.message}`, {
338
- rowIndex: handingRowIndex,
339
- rowData: rows[handingRowIndex],
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 ${handingRowIndex}`, {
343
- rowIndex: handingRowIndex,
344
- rowData: rows[handingRowIndex - (this.options.explain ? 2 : 1)],
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.1.0-beta.9",
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.2/xlsx-0.20.2.tgz"
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": "c3a2875e4cbbb43b1f2361e6f9f5f84a7d3f3c3c",
50
+ "gitHead": "303663aba6c6eefa27e6a6435b4c0352074ec40f",
50
51
  "keywords": [
51
52
  "Actions"
52
53
  ]