@adminforth/import-export 1.6.2 → 1.8.0

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/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { z } from "zod";
13
13
  const exportCsvBodySchema = z.object({
14
14
  filters: z.any(),
15
15
  sort: z.any(),
16
+ selectedIds: z.array(z.any()).optional(),
16
17
  }).strict();
17
18
  const importCsvBodySchema = z.object({
18
19
  data: z.record(z.string(), z.array(z.unknown())),
@@ -92,10 +93,7 @@ export default class ImportExport extends AdminForthPlugin {
92
93
  }
93
94
  resourceConfig.options.pageInjections.list.threeDotsDropdownItems.push({
94
95
  file: this.componentPath('ExportCsv.vue'),
95
- meta: { pluginInstanceId: this.pluginInstanceId, select: 'all' }
96
- }, {
97
- file: this.componentPath('ExportCsv.vue'),
98
- meta: { pluginInstanceId: this.pluginInstanceId, select: 'filtered' }
96
+ meta: { pluginInstanceId: this.pluginInstanceId }
99
97
  }, {
100
98
  file: this.componentPath('ImportCsv.vue'),
101
99
  meta: { pluginInstanceId: this.pluginInstanceId }
@@ -122,9 +120,8 @@ export default class ImportExport extends AdminForthPlugin {
122
120
  method: 'POST',
123
121
  path: `/plugin/${this.pluginInstanceId}/export-csv`,
124
122
  request_schema: exportCsvBodySchema,
125
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, response }) {
126
- const payload = body;
127
- const { filters, sort } = payload;
123
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers }) {
124
+ const { filters, sort, selectedIds } = body;
128
125
  if (!filters || !sort) {
129
126
  return { ok: false, error: 'Missing filters or sort in request body' };
130
127
  }
@@ -139,39 +136,19 @@ export default class ImportExport extends AdminForthPlugin {
139
136
  if (rawFilterError) {
140
137
  return rawFilterError;
141
138
  }
142
- const data = yield this.adminforth.connectors[this.resourceConfig.dataSource].getData({
143
- resource: this.resourceConfig,
144
- limit: 1e6,
145
- offset: 0,
146
- filters: this.adminforth.connectors[this.resourceConfig.dataSource].validateAndNormalizeInputFilters(filters),
147
- sort,
148
- getTotals: true,
149
- });
150
- // prepare data for PapaParse unparse
151
- const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
152
- const columnsToForceQuote = columns.map(col => {
153
- return col.type !== AdminForthDataTypes.FLOAT
154
- && col.type !== AdminForthDataTypes.INTEGER
155
- && col.type !== AdminForthDataTypes.BOOLEAN;
156
- });
157
- const fields = columns.map((col) => col.name);
158
- const rows = data.data.map((row) => {
159
- return columns.map((col) => {
160
- var _a;
161
- const value = row[col.name];
162
- if (col.type === AdminForthDataTypes.JSON || ((_a = col.isArray) === null || _a === void 0 ? void 0 : _a.enabled)) {
163
- return value == null ? value : JSON.stringify(value);
164
- }
165
- return value;
166
- });
167
- });
168
- this.tryToAuditLogAction('export', `Export CSV with filters: ${JSON.stringify(filters)} and sort: ${JSON.stringify(sort)}. Total records: ${rows.length}`, adminUser, headers);
169
- return {
170
- data: { fields, data: rows },
171
- columnsToForceQuote,
172
- exportedCount: data.total,
173
- ok: true
174
- };
139
+ let effectiveFilters = filters;
140
+ if (Array.isArray(selectedIds) && selectedIds.length > 0) {
141
+ const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
142
+ if (!primaryKeyColumn) {
143
+ return { ok: false, error: 'Cannot export selected records: resource has no primary key' };
144
+ }
145
+ effectiveFilters = [{
146
+ field: primaryKeyColumn.name,
147
+ operator: AdminForthFilterOperators.IN,
148
+ value: selectedIds,
149
+ }];
150
+ }
151
+ return this.exportCsv(effectiveFilters, sort, { adminUser, headers });
175
152
  })
176
153
  });
177
154
  server.endpoint({
@@ -179,8 +156,7 @@ export default class ImportExport extends AdminForthPlugin {
179
156
  path: `/plugin/${this.pluginInstanceId}/import-csv`,
180
157
  request_schema: importCsvBodySchema,
181
158
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, query, headers, cookies, requestUrl, response }) {
182
- const payload = body;
183
- const { data } = payload;
159
+ const { data } = body;
184
160
  if (!data || typeof data !== 'object') {
185
161
  return { ok: false, error: 'Invalid data format. Expected an object with column names as keys and arrays of values as values.' };
186
162
  }
@@ -191,61 +167,12 @@ export default class ImportExport extends AdminForthPlugin {
191
167
  if (!createEditAccess.ok) {
192
168
  return { ok: false, error: createEditAccess.error };
193
169
  }
194
- const columns = this.getColumnNames(data);
195
- const { errors, resourceColumns } = this.validateColumns(columns);
196
- const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
197
- if (errors.length > 0) {
198
- return { ok: false, errors };
199
- }
200
- const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
201
- const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
202
- console.log('Prepared rows for import:', rows);
203
- this.tryToAuditLogAction('import', `Import CSV with ${Object.keys(data).length} columns`, adminUser, headers);
204
- let importedCount = 0;
205
- let updatedCount = 0;
206
- const limit = pLimit(100);
207
- yield Promise.all(rows.map((row) => limit(() => __awaiter(this, void 0, void 0, function* () {
208
- try {
209
- const rowErrors = yield this.isRowValid(row);
210
- if (rowErrors.length > 0) {
211
- errors.push(...rowErrors);
212
- return;
213
- }
214
- const recordId = primaryKeyColumn ? row[primaryKeyColumn.name] : undefined;
215
- if (primaryKeyColumn && recordId) {
216
- const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
217
- .list([Filters.EQ(primaryKeyColumn.name, recordId)]);
218
- if (existingRecord.length > 0) {
219
- const connector = this.adminforth.connectors[resource.dataSource];
220
- const oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
221
- if (!oldRecord) {
222
- const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
223
- return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
224
- }
225
- const { error } = yield this.adminforth.updateResourceRecord({
226
- resource, updates: row, adminUser, oldRecord, recordId, response,
227
- extra: { body, query, headers, cookies, requestUrl, response }
228
- });
229
- if (error) {
230
- return { error };
231
- }
232
- updatedCount++;
233
- return;
234
- }
235
- }
236
- yield this.adminforth.createResourceRecord({
237
- resource: resource,
238
- record: row,
239
- adminUser: adminUser,
240
- extra: { body, query, headers, cookies, requestUrl, response }
241
- });
242
- importedCount++;
243
- }
244
- catch (e) {
245
- errors.push(e.message);
246
- }
247
- }))));
248
- return { ok: true, importedCount, updatedCount, errors };
170
+ return this.importCsv(data, {
171
+ adminUser,
172
+ headers,
173
+ response,
174
+ extra: { body, query, headers, cookies, requestUrl, response },
175
+ });
249
176
  })
250
177
  });
251
178
  server.endpoint({
@@ -253,8 +180,7 @@ export default class ImportExport extends AdminForthPlugin {
253
180
  path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
254
181
  request_schema: importCsvBodySchema,
255
182
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, query, headers, cookies, requestUrl, response }) {
256
- const payload = body;
257
- const { data } = payload;
183
+ const { data } = body;
258
184
  if (!data || typeof data !== 'object') {
259
185
  return { ok: false, error: 'Invalid data format. Expected an object with column names as keys and arrays of values as values.' };
260
186
  }
@@ -262,52 +188,19 @@ export default class ImportExport extends AdminForthPlugin {
262
188
  if (!access.ok) {
263
189
  return { ok: false, error: access.error };
264
190
  }
265
- const columns = this.getColumnNames(data);
266
- const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
267
- const { errors, resourceColumns } = this.validateColumns(columns);
268
- if (errors.length > 0) {
269
- return { ok: false, errors };
270
- }
271
- const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
272
- const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
273
- this.tryToAuditLogAction('import', `Import CSV (new only) with ${Object.keys(data).length} columns`, adminUser, headers);
274
- let importedCount = 0;
275
- const limit = pLimit(100);
276
- yield Promise.all(rows.map((row) => limit(() => __awaiter(this, void 0, void 0, function* () {
277
- try {
278
- const rowErrors = yield this.isRowValid(row);
279
- if (rowErrors.length > 0) {
280
- errors.push(...rowErrors);
281
- return;
282
- }
283
- if (primaryKeyColumn && row[primaryKeyColumn.name]) {
284
- const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
285
- .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
286
- if (existingRecord.length > 0) {
287
- return;
288
- }
289
- }
290
- yield this.adminforth.createResourceRecord({
291
- resource: resource,
292
- record: row,
293
- adminUser: adminUser,
294
- extra: { body, query, headers, cookies, requestUrl, response }
295
- });
296
- importedCount++;
297
- }
298
- catch (e) {
299
- errors.push(e.message);
300
- }
301
- }))));
302
- return { ok: true, importedCount, errors };
191
+ return this.importCsvNewOnly(data, {
192
+ adminUser,
193
+ headers,
194
+ extra: { body, query, headers, cookies, requestUrl, response },
195
+ });
303
196
  })
304
197
  });
305
198
  server.endpoint({
306
199
  method: 'POST',
307
200
  path: `/plugin/${this.pluginInstanceId}/check-records`,
308
201
  request_schema: importCsvBodySchema,
309
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
310
- const payload = body;
202
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
203
+ const { data } = body;
311
204
  const access = yield this.ensureAnyAllowed(adminUser, [
312
205
  { source: ActionCheckSource.ListRequest, action: AllowedActionsEnum.list },
313
206
  { source: ActionCheckSource.ShowRequest, action: AllowedActionsEnum.show },
@@ -315,29 +208,198 @@ export default class ImportExport extends AdminForthPlugin {
315
208
  if (!access.ok) {
316
209
  return { ok: false, error: access.error };
317
210
  }
318
- const { data } = payload;
319
- const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
320
- const columns = this.getColumnNames(data);
321
- const rows = this.buildRowsFromData(data, columns, undefined, { coerceTypes: false });
322
- const primaryKeys = rows
323
- .map(row => primaryKeyColumn ? row[primaryKeyColumn.name] : undefined)
324
- .filter(key => key !== undefined && key !== null && key !== '');
325
- const existingRecords = yield this.adminforth
326
- .resource(this.resourceConfig.resourceId)
327
- .list([{
328
- field: primaryKeyColumn.name,
329
- operator: AdminForthFilterOperators.IN,
330
- value: primaryKeys,
331
- }]);
332
- return {
333
- ok: true,
334
- total: rows.length,
335
- existingCount: existingRecords.length,
336
- newCount: rows.length - existingRecords.length,
337
- };
211
+ return this.checkRecords(data);
338
212
  })
339
213
  });
340
214
  }
215
+ /**
216
+ * Export resource records as CSV-ready data.
217
+ * Can be called programmatically, e.g. `this.exportCsv(filters, sort)`.
218
+ */
219
+ exportCsv(filters_1, sort_1) {
220
+ return __awaiter(this, arguments, void 0, function* (filters, sort, options = {}) {
221
+ const { adminUser, headers } = options;
222
+ const connector = this.adminforth.connectors[this.resourceConfig.dataSource];
223
+ const data = yield connector.getData({
224
+ resource: this.resourceConfig,
225
+ limit: 1e6,
226
+ offset: 0,
227
+ filters: connector.validateAndNormalizeInputFilters(filters),
228
+ sort,
229
+ getTotals: true,
230
+ });
231
+ // prepare data for PapaParse unparse
232
+ const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
233
+ const columnsToForceQuote = columns.map(col => {
234
+ return col.type !== AdminForthDataTypes.FLOAT
235
+ && col.type !== AdminForthDataTypes.INTEGER
236
+ && col.type !== AdminForthDataTypes.BOOLEAN;
237
+ });
238
+ const fields = columns.map((col) => col.name);
239
+ const rows = data.data.map((row) => {
240
+ return columns.map((col) => {
241
+ var _a;
242
+ const value = row[col.name];
243
+ if (col.type === AdminForthDataTypes.JSON || ((_a = col.isArray) === null || _a === void 0 ? void 0 : _a.enabled)) {
244
+ return value == null ? value : JSON.stringify(value);
245
+ }
246
+ return value;
247
+ });
248
+ });
249
+ if (adminUser) {
250
+ this.tryToAuditLogAction('export', `Export CSV with filters: ${JSON.stringify(filters)} and sort: ${JSON.stringify(sort)}. Total records: ${rows.length}`, adminUser, headers);
251
+ }
252
+ return {
253
+ ok: true,
254
+ data: { fields, data: rows },
255
+ columnsToForceQuote,
256
+ exportedCount: data.total,
257
+ };
258
+ });
259
+ }
260
+ /**
261
+ * Import records from column-oriented data, creating new records and updating
262
+ * existing ones (matched by primary key).
263
+ * Can be called programmatically, e.g. `this.importCsv(data, { adminUser })`.
264
+ */
265
+ importCsv(data_1) {
266
+ return __awaiter(this, arguments, void 0, function* (data, options = {}) {
267
+ const { adminUser, headers, extra, response } = options;
268
+ const columns = this.getColumnNames(data);
269
+ const { errors, resourceColumns } = this.validateColumns(columns);
270
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
271
+ if (errors.length > 0) {
272
+ return { ok: false, errors };
273
+ }
274
+ const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
275
+ const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
276
+ if (adminUser) {
277
+ this.tryToAuditLogAction('import', `Import CSV with ${Object.keys(data).length} columns`, adminUser, headers);
278
+ }
279
+ let importedCount = 0;
280
+ let updatedCount = 0;
281
+ const limit = pLimit(100);
282
+ yield Promise.all(rows.map((row) => limit(() => __awaiter(this, void 0, void 0, function* () {
283
+ try {
284
+ const rowErrors = yield this.isRowValid(row);
285
+ if (rowErrors.length > 0) {
286
+ errors.push(...rowErrors);
287
+ return;
288
+ }
289
+ const recordId = primaryKeyColumn ? row[primaryKeyColumn.name] : undefined;
290
+ if (primaryKeyColumn && recordId) {
291
+ const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
292
+ .list([Filters.EQ(primaryKeyColumn.name, recordId)]);
293
+ if (existingRecord.length > 0) {
294
+ const connector = this.adminforth.connectors[resource.dataSource];
295
+ const oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
296
+ if (!oldRecord) {
297
+ errors.push(`Record with ${primaryKeyColumn.name} ${recordId} not found`);
298
+ return;
299
+ }
300
+ const { error } = yield this.adminforth.updateResourceRecord({
301
+ resource, updates: row, adminUser, oldRecord, recordId, response,
302
+ extra,
303
+ });
304
+ if (error) {
305
+ errors.push(error);
306
+ return;
307
+ }
308
+ updatedCount++;
309
+ return;
310
+ }
311
+ }
312
+ yield this.adminforth.createResourceRecord({
313
+ resource: resource,
314
+ record: row,
315
+ adminUser: adminUser,
316
+ extra,
317
+ });
318
+ importedCount++;
319
+ }
320
+ catch (e) {
321
+ errors.push(e.message);
322
+ }
323
+ }))));
324
+ return { ok: true, importedCount, updatedCount, errors };
325
+ });
326
+ }
327
+ /**
328
+ * Import only records that do not already exist (matched by primary key).
329
+ * Can be called programmatically, e.g. `this.importCsvNewOnly(data, { adminUser })`.
330
+ */
331
+ importCsvNewOnly(data_1) {
332
+ return __awaiter(this, arguments, void 0, function* (data, options = {}) {
333
+ const { adminUser, headers, extra } = options;
334
+ const columns = this.getColumnNames(data);
335
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
336
+ const { errors, resourceColumns } = this.validateColumns(columns);
337
+ if (errors.length > 0) {
338
+ return { ok: false, errors };
339
+ }
340
+ const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
341
+ const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
342
+ if (adminUser) {
343
+ this.tryToAuditLogAction('import', `Import CSV (new only) with ${Object.keys(data).length} columns`, adminUser, headers);
344
+ }
345
+ let importedCount = 0;
346
+ const limit = pLimit(100);
347
+ yield Promise.all(rows.map((row) => limit(() => __awaiter(this, void 0, void 0, function* () {
348
+ try {
349
+ const rowErrors = yield this.isRowValid(row);
350
+ if (rowErrors.length > 0) {
351
+ errors.push(...rowErrors);
352
+ return;
353
+ }
354
+ if (primaryKeyColumn && row[primaryKeyColumn.name]) {
355
+ const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
356
+ .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
357
+ if (existingRecord.length > 0) {
358
+ return;
359
+ }
360
+ }
361
+ yield this.adminforth.createResourceRecord({
362
+ resource: resource,
363
+ record: row,
364
+ adminUser: adminUser,
365
+ extra,
366
+ });
367
+ importedCount++;
368
+ }
369
+ catch (e) {
370
+ errors.push(e.message);
371
+ }
372
+ }))));
373
+ return { ok: true, importedCount, errors };
374
+ });
375
+ }
376
+ /**
377
+ * Check how many of the given records already exist (matched by primary key).
378
+ * Can be called programmatically, e.g. `this.checkRecords(data)`.
379
+ */
380
+ checkRecords(data) {
381
+ return __awaiter(this, void 0, void 0, function* () {
382
+ const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
383
+ const columns = this.getColumnNames(data);
384
+ const rows = this.buildRowsFromData(data, columns, undefined, { coerceTypes: false });
385
+ const primaryKeys = rows
386
+ .map(row => primaryKeyColumn ? row[primaryKeyColumn.name] : undefined)
387
+ .filter(key => key !== undefined && key !== null && key !== '');
388
+ const existingRecords = yield this.adminforth
389
+ .resource(this.resourceConfig.resourceId)
390
+ .list([{
391
+ field: primaryKeyColumn.name,
392
+ operator: AdminForthFilterOperators.IN,
393
+ value: primaryKeys,
394
+ }]);
395
+ return {
396
+ ok: true,
397
+ total: rows.length,
398
+ existingCount: existingRecords.length,
399
+ newCount: rows.length - existingRecords.length,
400
+ };
401
+ });
402
+ }
341
403
  getColumnNames(data) {
342
404
  return Object.keys(data !== null && data !== void 0 ? data : {});
343
405
  }