@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.
@@ -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
  }
@@ -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: 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,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
- private getExpectedHeaders;
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
- const columns = this.getColumnsByPermission(ctx);
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
- let transaction = options.transaction;
98
- if (!transaction && this.options.collectionManager.db) {
99
- 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
+ }
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
- (_a = this.logger) == null ? void 0 : _a.info(`Import completed successfully, imported ${imported} records`);
111
- if (this.options.collectionManager.db) {
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(options),
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
- transaction && await transaction.rollback();
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.db;
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
- for (const chunkRows of chunks) {
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, _c;
319
+ var _a, _b;
251
320
  let { handingRowIndex = 1 } = options;
252
- const { transaction } = runOptions;
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 ((_b = error.params) == null ? void 0 : _b.rowIndex) {
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
- (_c = this.logger) == null ? void 0 : _c.error(`Import error at row ${handingRowIndex}: ${error.message}`, {
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.1",
10
- "license": "AGPL-3.0",
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": "^10.6.0",
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": "d27baf21569643d6fa83f882233f4e90eb5b89f1",
49
+ "gitHead": "bb96d633a6371afb586072ff516bd0613c757db0",
43
50
  "keywords": [
44
51
  "Actions"
45
52
  ]