@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/build.log +2 -2
- package/custom/ExportCsv.vue +107 -16
- package/dist/custom/ExportCsv.vue +107 -16
- package/dist/index.d.ts +56 -1
- package/dist/index.js +221 -159
- package/index.ts +265 -176
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AdminForthPlugin, suggestIfTypo, AdminForthFilterOperators, Filters, AdminForthDataTypes, rejectApiRawFilters, interpretResource, ActionCheckSource, AllowedActionsEnum } from "adminforth";
|
|
2
|
-
import type { IAdminForth, IHttpServer, AdminForthResourceColumn, AdminForthComponentDeclaration, AdminForthResource, AdminUser } from "adminforth";
|
|
2
|
+
import type { IAdminForth, IHttpServer, AdminForthResourceColumn, AdminForthComponentDeclaration, AdminForthResource, AdminUser, HttpExtra, IAdminForthHttpResponse } from "adminforth";
|
|
3
3
|
import type { PluginOptions } from './types.js';
|
|
4
4
|
import pLimit from 'p-limit';
|
|
5
5
|
import { z } from "zod";
|
|
@@ -7,6 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
const exportCsvBodySchema = z.object({
|
|
8
8
|
filters: z.any(),
|
|
9
9
|
sort: z.any(),
|
|
10
|
+
selectedIds: z.array(z.any()).optional(),
|
|
10
11
|
}).strict();
|
|
11
12
|
|
|
12
13
|
const importCsvBodySchema = z.object({
|
|
@@ -20,7 +21,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
20
21
|
adminforth: IAdminForth;
|
|
21
22
|
auditLogPlugin: Record<string, any> | undefined;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
constructor(options: PluginOptions) {
|
|
25
26
|
super(options, import.meta.url);
|
|
26
27
|
this.options = options;
|
|
@@ -105,10 +106,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
105
106
|
}
|
|
106
107
|
(resourceConfig.options.pageInjections.list.threeDotsDropdownItems as AdminForthComponentDeclaration[]).push({
|
|
107
108
|
file: this.componentPath('ExportCsv.vue'),
|
|
108
|
-
meta: { pluginInstanceId: this.pluginInstanceId
|
|
109
|
-
}, {
|
|
110
|
-
file: this.componentPath('ExportCsv.vue'),
|
|
111
|
-
meta: { pluginInstanceId: this.pluginInstanceId, select: 'filtered' }
|
|
109
|
+
meta: { pluginInstanceId: this.pluginInstanceId }
|
|
112
110
|
}, {
|
|
113
111
|
file: this.componentPath('ImportCsv.vue'),
|
|
114
112
|
meta: { pluginInstanceId: this.pluginInstanceId }
|
|
@@ -138,9 +136,8 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
138
136
|
method: 'POST',
|
|
139
137
|
path: `/plugin/${this.pluginInstanceId}/export-csv`,
|
|
140
138
|
request_schema: exportCsvBodySchema,
|
|
141
|
-
handler: async ({ body, adminUser, headers
|
|
142
|
-
const
|
|
143
|
-
const { filters, sort } = payload;
|
|
139
|
+
handler: async ({ body, adminUser, headers }) => {
|
|
140
|
+
const { filters, sort, selectedIds } = body as z.infer<typeof exportCsvBodySchema>;
|
|
144
141
|
if (!filters || !sort) {
|
|
145
142
|
return { ok: false, error: 'Missing filters or sort in request body' };
|
|
146
143
|
}
|
|
@@ -159,44 +156,21 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
159
156
|
if (rawFilterError) {
|
|
160
157
|
return rawFilterError;
|
|
161
158
|
}
|
|
162
|
-
const data = await this.adminforth.connectors[this.resourceConfig.dataSource].getData({
|
|
163
|
-
resource: this.resourceConfig,
|
|
164
|
-
limit: 1e6,
|
|
165
|
-
offset: 0,
|
|
166
|
-
filters: this.adminforth.connectors[this.resourceConfig.dataSource].validateAndNormalizeInputFilters(filters),
|
|
167
|
-
sort,
|
|
168
|
-
getTotals: true,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// prepare data for PapaParse unparse
|
|
172
|
-
const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
|
|
173
|
-
|
|
174
|
-
const columnsToForceQuote = columns.map(col => {
|
|
175
|
-
return col.type !== AdminForthDataTypes.FLOAT
|
|
176
|
-
&& col.type !== AdminForthDataTypes.INTEGER
|
|
177
|
-
&& col.type !== AdminForthDataTypes.BOOLEAN;
|
|
178
|
-
})
|
|
179
159
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this.tryToAuditLogAction('export', `Export CSV with filters: ${JSON.stringify(filters)} and sort: ${JSON.stringify(sort)}. Total records: ${rows.length}`, adminUser, headers);
|
|
160
|
+
let effectiveFilters = filters;
|
|
161
|
+
if (Array.isArray(selectedIds) && selectedIds.length > 0) {
|
|
162
|
+
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
163
|
+
if (!primaryKeyColumn) {
|
|
164
|
+
return { ok: false, error: 'Cannot export selected records: resource has no primary key' };
|
|
165
|
+
}
|
|
166
|
+
effectiveFilters = [{
|
|
167
|
+
field: primaryKeyColumn.name,
|
|
168
|
+
operator: AdminForthFilterOperators.IN,
|
|
169
|
+
value: selectedIds,
|
|
170
|
+
}];
|
|
171
|
+
}
|
|
193
172
|
|
|
194
|
-
return {
|
|
195
|
-
data: { fields, data: rows },
|
|
196
|
-
columnsToForceQuote,
|
|
197
|
-
exportedCount: data.total,
|
|
198
|
-
ok: true
|
|
199
|
-
};
|
|
173
|
+
return this.exportCsv(effectiveFilters, sort, { adminUser, headers });
|
|
200
174
|
}
|
|
201
175
|
});
|
|
202
176
|
|
|
@@ -205,8 +179,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
205
179
|
path: `/plugin/${this.pluginInstanceId}/import-csv`,
|
|
206
180
|
request_schema: importCsvBodySchema,
|
|
207
181
|
handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
|
|
208
|
-
const
|
|
209
|
-
const { data } = payload;
|
|
182
|
+
const { data } = body as z.infer<typeof importCsvBodySchema>;
|
|
210
183
|
if (!data || typeof data !== 'object') {
|
|
211
184
|
return { ok: false, error: 'Invalid data format. Expected an object with column names as keys and arrays of values as values.' };
|
|
212
185
|
}
|
|
@@ -221,66 +194,12 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
221
194
|
if (!createEditAccess.ok) {
|
|
222
195
|
return { ok: false, error: createEditAccess.error };
|
|
223
196
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
232
|
-
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
233
|
-
|
|
234
|
-
console.log('Prepared rows for import:', rows);
|
|
235
|
-
this.tryToAuditLogAction('import', `Import CSV with ${Object.keys(data).length} columns`, adminUser, headers);
|
|
236
|
-
|
|
237
|
-
let importedCount = 0;
|
|
238
|
-
let updatedCount = 0;
|
|
239
|
-
const limit = pLimit(100);
|
|
240
|
-
|
|
241
|
-
await Promise.all(rows.map((row) => limit(async () => {
|
|
242
|
-
try {
|
|
243
|
-
const rowErrors = await this.isRowValid(row);
|
|
244
|
-
if (rowErrors.length > 0) {
|
|
245
|
-
errors.push(...rowErrors);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const recordId = primaryKeyColumn ? row[primaryKeyColumn.name] as string : undefined;
|
|
249
|
-
if (primaryKeyColumn && recordId) {
|
|
250
|
-
const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
251
|
-
.list([Filters.EQ(primaryKeyColumn.name, recordId)]);
|
|
252
|
-
|
|
253
|
-
if (existingRecord.length > 0) {
|
|
254
|
-
const connector = this.adminforth.connectors[resource.dataSource];
|
|
255
|
-
const oldRecord = await connector.getRecordByPrimaryKey(resource, recordId)
|
|
256
|
-
if (!oldRecord) {
|
|
257
|
-
const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
|
|
258
|
-
return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
|
|
259
|
-
}
|
|
260
|
-
const { error } = await this.adminforth.updateResourceRecord({
|
|
261
|
-
resource, updates: row, adminUser, oldRecord, recordId, response,
|
|
262
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
263
|
-
});
|
|
264
|
-
if (error) {
|
|
265
|
-
return { error };
|
|
266
|
-
}
|
|
267
|
-
updatedCount++;
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
await this.adminforth.createResourceRecord({
|
|
272
|
-
resource: resource,
|
|
273
|
-
record: row,
|
|
274
|
-
adminUser: adminUser,
|
|
275
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
276
|
-
});
|
|
277
|
-
importedCount++;
|
|
278
|
-
} catch (e) {
|
|
279
|
-
errors.push(e.message);
|
|
280
|
-
}
|
|
281
|
-
})));
|
|
282
|
-
|
|
283
|
-
return { ok: true, importedCount, updatedCount, errors };
|
|
197
|
+
return this.importCsv(data, {
|
|
198
|
+
adminUser,
|
|
199
|
+
headers,
|
|
200
|
+
response,
|
|
201
|
+
extra: { body, query, headers, cookies, requestUrl, response },
|
|
202
|
+
});
|
|
284
203
|
}
|
|
285
204
|
});
|
|
286
205
|
|
|
@@ -289,8 +208,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
289
208
|
path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
|
|
290
209
|
request_schema: importCsvBodySchema,
|
|
291
210
|
handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
|
|
292
|
-
const
|
|
293
|
-
const { data } = payload;
|
|
211
|
+
const { data } = body as z.infer<typeof importCsvBodySchema>;
|
|
294
212
|
if (!data || typeof data !== 'object') {
|
|
295
213
|
return { ok: false, error: 'Invalid data format. Expected an object with column names as keys and arrays of values as values.' };
|
|
296
214
|
}
|
|
@@ -302,48 +220,11 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
302
220
|
if (!access.ok) {
|
|
303
221
|
return { ok: false, error: access.error };
|
|
304
222
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
313
|
-
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
314
|
-
this.tryToAuditLogAction('import', `Import CSV (new only) with ${Object.keys(data).length} columns`, adminUser, headers);
|
|
315
|
-
|
|
316
|
-
let importedCount = 0;
|
|
317
|
-
const limit = pLimit(100);
|
|
318
|
-
|
|
319
|
-
await Promise.all(rows.map((row) => limit(async () => {
|
|
320
|
-
try {
|
|
321
|
-
const rowErrors = await this.isRowValid(row);
|
|
322
|
-
if (rowErrors.length > 0) {
|
|
323
|
-
errors.push(...rowErrors);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (primaryKeyColumn && row[primaryKeyColumn.name]) {
|
|
327
|
-
const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
328
|
-
.list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
|
|
329
|
-
|
|
330
|
-
if (existingRecord.length > 0) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
await this.adminforth.createResourceRecord({
|
|
335
|
-
resource: resource,
|
|
336
|
-
record: row,
|
|
337
|
-
adminUser: adminUser,
|
|
338
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
339
|
-
});
|
|
340
|
-
importedCount++;
|
|
341
|
-
} catch (e) {
|
|
342
|
-
errors.push(e.message);
|
|
343
|
-
}
|
|
344
|
-
})));
|
|
345
|
-
|
|
346
|
-
return { ok: true, importedCount, errors };
|
|
223
|
+
return this.importCsvNewOnly(data, {
|
|
224
|
+
adminUser,
|
|
225
|
+
headers,
|
|
226
|
+
extra: { body, query, headers, cookies, requestUrl, response },
|
|
227
|
+
});
|
|
347
228
|
}
|
|
348
229
|
});
|
|
349
230
|
|
|
@@ -351,8 +232,8 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
351
232
|
method: 'POST',
|
|
352
233
|
path: `/plugin/${this.pluginInstanceId}/check-records`,
|
|
353
234
|
request_schema: importCsvBodySchema,
|
|
354
|
-
handler: async ({ body, adminUser
|
|
355
|
-
const
|
|
235
|
+
handler: async ({ body, adminUser }) => {
|
|
236
|
+
const { data } = body as z.infer<typeof importCsvBodySchema>;
|
|
356
237
|
const access = await this.ensureAnyAllowed(
|
|
357
238
|
adminUser,
|
|
358
239
|
[
|
|
@@ -364,32 +245,240 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
364
245
|
if (!access.ok) {
|
|
365
246
|
return { ok: false, error: access.error };
|
|
366
247
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const primaryKeys = rows
|
|
373
|
-
.map(row => primaryKeyColumn ? row[primaryKeyColumn.name] : undefined)
|
|
374
|
-
.filter(key => key !== undefined && key !== null && key !== '');
|
|
375
|
-
|
|
376
|
-
const existingRecords = await this.adminforth
|
|
377
|
-
.resource(this.resourceConfig.resourceId)
|
|
378
|
-
.list([{
|
|
379
|
-
field: primaryKeyColumn.name,
|
|
380
|
-
operator: AdminForthFilterOperators.IN,
|
|
381
|
-
value: primaryKeys,
|
|
382
|
-
}]);
|
|
248
|
+
return this.checkRecords(data);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
383
252
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Export resource records as CSV-ready data.
|
|
255
|
+
* Can be called programmatically, e.g. `this.exportCsv(filters, sort)`.
|
|
256
|
+
*/
|
|
257
|
+
public async exportCsv(
|
|
258
|
+
filters: any,
|
|
259
|
+
sort: any,
|
|
260
|
+
options: { adminUser?: AdminUser; headers?: Record<string, string> } = {}
|
|
261
|
+
): Promise<{
|
|
262
|
+
ok: true;
|
|
263
|
+
data: { fields: string[]; data: unknown[][] };
|
|
264
|
+
columnsToForceQuote: boolean[];
|
|
265
|
+
exportedCount: number;
|
|
266
|
+
}> {
|
|
267
|
+
const { adminUser, headers } = options;
|
|
268
|
+
const connector = this.adminforth.connectors[this.resourceConfig.dataSource];
|
|
269
|
+
const data = await connector.getData({
|
|
270
|
+
resource: this.resourceConfig,
|
|
271
|
+
limit: 1e6,
|
|
272
|
+
offset: 0,
|
|
273
|
+
filters: connector.validateAndNormalizeInputFilters(filters),
|
|
274
|
+
sort,
|
|
275
|
+
getTotals: true,
|
|
276
|
+
});
|
|
390
277
|
|
|
391
|
-
|
|
278
|
+
// prepare data for PapaParse unparse
|
|
279
|
+
const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
|
|
280
|
+
|
|
281
|
+
const columnsToForceQuote = columns.map(col => {
|
|
282
|
+
return col.type !== AdminForthDataTypes.FLOAT
|
|
283
|
+
&& col.type !== AdminForthDataTypes.INTEGER
|
|
284
|
+
&& col.type !== AdminForthDataTypes.BOOLEAN;
|
|
392
285
|
});
|
|
286
|
+
|
|
287
|
+
const fields = columns.map((col) => col.name);
|
|
288
|
+
|
|
289
|
+
const rows = data.data.map((row) => {
|
|
290
|
+
return columns.map((col) => {
|
|
291
|
+
const value = row[col.name];
|
|
292
|
+
if (col.type === AdminForthDataTypes.JSON || col.isArray?.enabled) {
|
|
293
|
+
return value == null ? value : JSON.stringify(value);
|
|
294
|
+
}
|
|
295
|
+
return value;
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (adminUser) {
|
|
300
|
+
this.tryToAuditLogAction('export', `Export CSV with filters: ${JSON.stringify(filters)} and sort: ${JSON.stringify(sort)}. Total records: ${rows.length}`, adminUser, headers);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
data: { fields, data: rows },
|
|
306
|
+
columnsToForceQuote,
|
|
307
|
+
exportedCount: data.total,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Import records from column-oriented data, creating new records and updating
|
|
313
|
+
* existing ones (matched by primary key).
|
|
314
|
+
* Can be called programmatically, e.g. `this.importCsv(data, { adminUser })`.
|
|
315
|
+
*/
|
|
316
|
+
public async importCsv(
|
|
317
|
+
data: Record<string, unknown[]>,
|
|
318
|
+
options: {
|
|
319
|
+
adminUser?: AdminUser;
|
|
320
|
+
headers?: Record<string, string>;
|
|
321
|
+
extra?: HttpExtra;
|
|
322
|
+
response?: IAdminForthHttpResponse;
|
|
323
|
+
} = {}
|
|
324
|
+
): Promise<{ ok: boolean; importedCount?: number; updatedCount?: number; errors: string[] }> {
|
|
325
|
+
const { adminUser, headers, extra, response } = options;
|
|
326
|
+
const columns = this.getColumnNames(data);
|
|
327
|
+
const { errors, resourceColumns } = this.validateColumns(columns);
|
|
328
|
+
const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
|
|
329
|
+
|
|
330
|
+
if (errors.length > 0) {
|
|
331
|
+
return { ok: false, errors };
|
|
332
|
+
}
|
|
333
|
+
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
334
|
+
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
335
|
+
|
|
336
|
+
if (adminUser) {
|
|
337
|
+
this.tryToAuditLogAction('import', `Import CSV with ${Object.keys(data).length} columns`, adminUser, headers);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let importedCount = 0;
|
|
341
|
+
let updatedCount = 0;
|
|
342
|
+
const limit = pLimit(100);
|
|
343
|
+
|
|
344
|
+
await Promise.all(rows.map((row) => limit(async () => {
|
|
345
|
+
try {
|
|
346
|
+
const rowErrors = await this.isRowValid(row);
|
|
347
|
+
if (rowErrors.length > 0) {
|
|
348
|
+
errors.push(...rowErrors);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const recordId = primaryKeyColumn ? row[primaryKeyColumn.name] as string : undefined;
|
|
352
|
+
if (primaryKeyColumn && recordId) {
|
|
353
|
+
const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
354
|
+
.list([Filters.EQ(primaryKeyColumn.name, recordId)]);
|
|
355
|
+
|
|
356
|
+
if (existingRecord.length > 0) {
|
|
357
|
+
const connector = this.adminforth.connectors[resource.dataSource];
|
|
358
|
+
const oldRecord = await connector.getRecordByPrimaryKey(resource, recordId);
|
|
359
|
+
if (!oldRecord) {
|
|
360
|
+
errors.push(`Record with ${primaryKeyColumn.name} ${recordId} not found`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const { error } = await this.adminforth.updateResourceRecord({
|
|
364
|
+
resource, updates: row, adminUser, oldRecord, recordId, response,
|
|
365
|
+
extra,
|
|
366
|
+
});
|
|
367
|
+
if (error) {
|
|
368
|
+
errors.push(error);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
updatedCount++;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
await this.adminforth.createResourceRecord({
|
|
376
|
+
resource: resource,
|
|
377
|
+
record: row,
|
|
378
|
+
adminUser: adminUser,
|
|
379
|
+
extra,
|
|
380
|
+
});
|
|
381
|
+
importedCount++;
|
|
382
|
+
} catch (e) {
|
|
383
|
+
errors.push(e.message);
|
|
384
|
+
}
|
|
385
|
+
})));
|
|
386
|
+
|
|
387
|
+
return { ok: true, importedCount, updatedCount, errors };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Import only records that do not already exist (matched by primary key).
|
|
392
|
+
* Can be called programmatically, e.g. `this.importCsvNewOnly(data, { adminUser })`.
|
|
393
|
+
*/
|
|
394
|
+
public async importCsvNewOnly(
|
|
395
|
+
data: Record<string, unknown[]>,
|
|
396
|
+
options: {
|
|
397
|
+
adminUser?: AdminUser;
|
|
398
|
+
headers?: Record<string, string>;
|
|
399
|
+
extra?: HttpExtra;
|
|
400
|
+
} = {}
|
|
401
|
+
): Promise<{ ok: boolean; importedCount?: number; errors: string[] }> {
|
|
402
|
+
const { adminUser, headers, extra } = options;
|
|
403
|
+
const columns = this.getColumnNames(data);
|
|
404
|
+
const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
|
|
405
|
+
const { errors, resourceColumns } = this.validateColumns(columns);
|
|
406
|
+
if (errors.length > 0) {
|
|
407
|
+
return { ok: false, errors };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
411
|
+
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
412
|
+
|
|
413
|
+
if (adminUser) {
|
|
414
|
+
this.tryToAuditLogAction('import', `Import CSV (new only) with ${Object.keys(data).length} columns`, adminUser, headers);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let importedCount = 0;
|
|
418
|
+
const limit = pLimit(100);
|
|
419
|
+
|
|
420
|
+
await Promise.all(rows.map((row) => limit(async () => {
|
|
421
|
+
try {
|
|
422
|
+
const rowErrors = await this.isRowValid(row);
|
|
423
|
+
if (rowErrors.length > 0) {
|
|
424
|
+
errors.push(...rowErrors);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (primaryKeyColumn && row[primaryKeyColumn.name]) {
|
|
428
|
+
const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
429
|
+
.list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
|
|
430
|
+
|
|
431
|
+
if (existingRecord.length > 0) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
await this.adminforth.createResourceRecord({
|
|
436
|
+
resource: resource,
|
|
437
|
+
record: row,
|
|
438
|
+
adminUser: adminUser,
|
|
439
|
+
extra,
|
|
440
|
+
});
|
|
441
|
+
importedCount++;
|
|
442
|
+
} catch (e) {
|
|
443
|
+
errors.push(e.message);
|
|
444
|
+
}
|
|
445
|
+
})));
|
|
446
|
+
|
|
447
|
+
return { ok: true, importedCount, errors };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Check how many of the given records already exist (matched by primary key).
|
|
452
|
+
* Can be called programmatically, e.g. `this.checkRecords(data)`.
|
|
453
|
+
*/
|
|
454
|
+
public async checkRecords(data: Record<string, unknown[]>): Promise<{
|
|
455
|
+
ok: true;
|
|
456
|
+
total: number;
|
|
457
|
+
existingCount: number;
|
|
458
|
+
newCount: number;
|
|
459
|
+
}> {
|
|
460
|
+
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
461
|
+
const columns = this.getColumnNames(data);
|
|
462
|
+
const rows = this.buildRowsFromData(data, columns, undefined, { coerceTypes: false });
|
|
463
|
+
|
|
464
|
+
const primaryKeys = rows
|
|
465
|
+
.map(row => primaryKeyColumn ? row[primaryKeyColumn.name] : undefined)
|
|
466
|
+
.filter(key => key !== undefined && key !== null && key !== '');
|
|
467
|
+
|
|
468
|
+
const existingRecords = await this.adminforth
|
|
469
|
+
.resource(this.resourceConfig.resourceId)
|
|
470
|
+
.list([{
|
|
471
|
+
field: primaryKeyColumn.name,
|
|
472
|
+
operator: AdminForthFilterOperators.IN,
|
|
473
|
+
value: primaryKeys,
|
|
474
|
+
}]);
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
ok: true,
|
|
478
|
+
total: rows.length,
|
|
479
|
+
existingCount: existingRecords.length,
|
|
480
|
+
newCount: rows.length - existingRecords.length,
|
|
481
|
+
};
|
|
393
482
|
}
|
|
394
483
|
|
|
395
484
|
private getColumnNames(data: Record<string, unknown[]>): string[] {
|