@adminforth/import-export 1.7.0 → 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.d.ts +56 -1
- package/dist/index.js +207 -155
- package/index.ts +251 -174
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AdminForthPlugin } from "adminforth";
|
|
2
|
-
import type { IAdminForth, IHttpServer, AdminForthResourceColumn, AdminForthResource } from "adminforth";
|
|
2
|
+
import type { IAdminForth, IHttpServer, AdminForthResourceColumn, AdminForthResource, AdminUser, HttpExtra, IAdminForthHttpResponse } from "adminforth";
|
|
3
3
|
import type { PluginOptions } from './types.js';
|
|
4
4
|
export default class ImportExport extends AdminForthPlugin {
|
|
5
5
|
options: PluginOptions;
|
|
@@ -15,6 +15,61 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
15
15
|
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
|
|
16
16
|
instanceUniqueRepresentation(pluginOptions: any): string;
|
|
17
17
|
setupEndpoints(server: IHttpServer): void;
|
|
18
|
+
/**
|
|
19
|
+
* Export resource records as CSV-ready data.
|
|
20
|
+
* Can be called programmatically, e.g. `this.exportCsv(filters, sort)`.
|
|
21
|
+
*/
|
|
22
|
+
exportCsv(filters: any, sort: any, options?: {
|
|
23
|
+
adminUser?: AdminUser;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
ok: true;
|
|
27
|
+
data: {
|
|
28
|
+
fields: string[];
|
|
29
|
+
data: unknown[][];
|
|
30
|
+
};
|
|
31
|
+
columnsToForceQuote: boolean[];
|
|
32
|
+
exportedCount: number;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Import records from column-oriented data, creating new records and updating
|
|
36
|
+
* existing ones (matched by primary key).
|
|
37
|
+
* Can be called programmatically, e.g. `this.importCsv(data, { adminUser })`.
|
|
38
|
+
*/
|
|
39
|
+
importCsv(data: Record<string, unknown[]>, options?: {
|
|
40
|
+
adminUser?: AdminUser;
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
extra?: HttpExtra;
|
|
43
|
+
response?: IAdminForthHttpResponse;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
ok: boolean;
|
|
46
|
+
importedCount?: number;
|
|
47
|
+
updatedCount?: number;
|
|
48
|
+
errors: string[];
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Import only records that do not already exist (matched by primary key).
|
|
52
|
+
* Can be called programmatically, e.g. `this.importCsvNewOnly(data, { adminUser })`.
|
|
53
|
+
*/
|
|
54
|
+
importCsvNewOnly(data: Record<string, unknown[]>, options?: {
|
|
55
|
+
adminUser?: AdminUser;
|
|
56
|
+
headers?: Record<string, string>;
|
|
57
|
+
extra?: HttpExtra;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
ok: boolean;
|
|
60
|
+
importedCount?: number;
|
|
61
|
+
errors: string[];
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Check how many of the given records already exist (matched by primary key).
|
|
65
|
+
* Can be called programmatically, e.g. `this.checkRecords(data)`.
|
|
66
|
+
*/
|
|
67
|
+
checkRecords(data: Record<string, unknown[]>): Promise<{
|
|
68
|
+
ok: true;
|
|
69
|
+
total: number;
|
|
70
|
+
existingCount: number;
|
|
71
|
+
newCount: number;
|
|
72
|
+
}>;
|
|
18
73
|
private getColumnNames;
|
|
19
74
|
private validateColumns;
|
|
20
75
|
private buildRowsFromData;
|
package/dist/index.js
CHANGED
|
@@ -120,9 +120,8 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
120
120
|
method: 'POST',
|
|
121
121
|
path: `/plugin/${this.pluginInstanceId}/export-csv`,
|
|
122
122
|
request_schema: exportCsvBodySchema,
|
|
123
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers
|
|
124
|
-
const
|
|
125
|
-
const { filters, sort, selectedIds } = payload;
|
|
123
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers }) {
|
|
124
|
+
const { filters, sort, selectedIds } = body;
|
|
126
125
|
if (!filters || !sort) {
|
|
127
126
|
return { ok: false, error: 'Missing filters or sort in request body' };
|
|
128
127
|
}
|
|
@@ -149,39 +148,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
149
148
|
value: selectedIds,
|
|
150
149
|
}];
|
|
151
150
|
}
|
|
152
|
-
|
|
153
|
-
resource: this.resourceConfig,
|
|
154
|
-
limit: 1e6,
|
|
155
|
-
offset: 0,
|
|
156
|
-
filters: this.adminforth.connectors[this.resourceConfig.dataSource].validateAndNormalizeInputFilters(effectiveFilters),
|
|
157
|
-
sort,
|
|
158
|
-
getTotals: true,
|
|
159
|
-
});
|
|
160
|
-
// prepare data for PapaParse unparse
|
|
161
|
-
const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
|
|
162
|
-
const columnsToForceQuote = columns.map(col => {
|
|
163
|
-
return col.type !== AdminForthDataTypes.FLOAT
|
|
164
|
-
&& col.type !== AdminForthDataTypes.INTEGER
|
|
165
|
-
&& col.type !== AdminForthDataTypes.BOOLEAN;
|
|
166
|
-
});
|
|
167
|
-
const fields = columns.map((col) => col.name);
|
|
168
|
-
const rows = data.data.map((row) => {
|
|
169
|
-
return columns.map((col) => {
|
|
170
|
-
var _a;
|
|
171
|
-
const value = row[col.name];
|
|
172
|
-
if (col.type === AdminForthDataTypes.JSON || ((_a = col.isArray) === null || _a === void 0 ? void 0 : _a.enabled)) {
|
|
173
|
-
return value == null ? value : JSON.stringify(value);
|
|
174
|
-
}
|
|
175
|
-
return value;
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
this.tryToAuditLogAction('export', `Export CSV with filters: ${JSON.stringify(effectiveFilters)} and sort: ${JSON.stringify(sort)}. Total records: ${rows.length}`, adminUser, headers);
|
|
179
|
-
return {
|
|
180
|
-
data: { fields, data: rows },
|
|
181
|
-
columnsToForceQuote,
|
|
182
|
-
exportedCount: data.total,
|
|
183
|
-
ok: true
|
|
184
|
-
};
|
|
151
|
+
return this.exportCsv(effectiveFilters, sort, { adminUser, headers });
|
|
185
152
|
})
|
|
186
153
|
});
|
|
187
154
|
server.endpoint({
|
|
@@ -189,8 +156,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
189
156
|
path: `/plugin/${this.pluginInstanceId}/import-csv`,
|
|
190
157
|
request_schema: importCsvBodySchema,
|
|
191
158
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, query, headers, cookies, requestUrl, response }) {
|
|
192
|
-
const
|
|
193
|
-
const { data } = payload;
|
|
159
|
+
const { data } = body;
|
|
194
160
|
if (!data || typeof data !== 'object') {
|
|
195
161
|
return { ok: false, error: 'Invalid data format. Expected an object with column names as keys and arrays of values as values.' };
|
|
196
162
|
}
|
|
@@ -201,61 +167,12 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
201
167
|
if (!createEditAccess.ok) {
|
|
202
168
|
return { ok: false, error: createEditAccess.error };
|
|
203
169
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
211
|
-
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
212
|
-
console.log('Prepared rows for import:', rows);
|
|
213
|
-
this.tryToAuditLogAction('import', `Import CSV with ${Object.keys(data).length} columns`, adminUser, headers);
|
|
214
|
-
let importedCount = 0;
|
|
215
|
-
let updatedCount = 0;
|
|
216
|
-
const limit = pLimit(100);
|
|
217
|
-
yield Promise.all(rows.map((row) => limit(() => __awaiter(this, void 0, void 0, function* () {
|
|
218
|
-
try {
|
|
219
|
-
const rowErrors = yield this.isRowValid(row);
|
|
220
|
-
if (rowErrors.length > 0) {
|
|
221
|
-
errors.push(...rowErrors);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const recordId = primaryKeyColumn ? row[primaryKeyColumn.name] : undefined;
|
|
225
|
-
if (primaryKeyColumn && recordId) {
|
|
226
|
-
const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
|
|
227
|
-
.list([Filters.EQ(primaryKeyColumn.name, recordId)]);
|
|
228
|
-
if (existingRecord.length > 0) {
|
|
229
|
-
const connector = this.adminforth.connectors[resource.dataSource];
|
|
230
|
-
const oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
|
|
231
|
-
if (!oldRecord) {
|
|
232
|
-
const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
|
|
233
|
-
return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
|
|
234
|
-
}
|
|
235
|
-
const { error } = yield this.adminforth.updateResourceRecord({
|
|
236
|
-
resource, updates: row, adminUser, oldRecord, recordId, response,
|
|
237
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
238
|
-
});
|
|
239
|
-
if (error) {
|
|
240
|
-
return { error };
|
|
241
|
-
}
|
|
242
|
-
updatedCount++;
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
yield this.adminforth.createResourceRecord({
|
|
247
|
-
resource: resource,
|
|
248
|
-
record: row,
|
|
249
|
-
adminUser: adminUser,
|
|
250
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
251
|
-
});
|
|
252
|
-
importedCount++;
|
|
253
|
-
}
|
|
254
|
-
catch (e) {
|
|
255
|
-
errors.push(e.message);
|
|
256
|
-
}
|
|
257
|
-
}))));
|
|
258
|
-
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
|
+
});
|
|
259
176
|
})
|
|
260
177
|
});
|
|
261
178
|
server.endpoint({
|
|
@@ -263,8 +180,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
263
180
|
path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
|
|
264
181
|
request_schema: importCsvBodySchema,
|
|
265
182
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, query, headers, cookies, requestUrl, response }) {
|
|
266
|
-
const
|
|
267
|
-
const { data } = payload;
|
|
183
|
+
const { data } = body;
|
|
268
184
|
if (!data || typeof data !== 'object') {
|
|
269
185
|
return { ok: false, error: 'Invalid data format. Expected an object with column names as keys and arrays of values as values.' };
|
|
270
186
|
}
|
|
@@ -272,52 +188,19 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
272
188
|
if (!access.ok) {
|
|
273
189
|
return { ok: false, error: access.error };
|
|
274
190
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
282
|
-
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
283
|
-
this.tryToAuditLogAction('import', `Import CSV (new only) with ${Object.keys(data).length} columns`, adminUser, headers);
|
|
284
|
-
let importedCount = 0;
|
|
285
|
-
const limit = pLimit(100);
|
|
286
|
-
yield Promise.all(rows.map((row) => limit(() => __awaiter(this, void 0, void 0, function* () {
|
|
287
|
-
try {
|
|
288
|
-
const rowErrors = yield this.isRowValid(row);
|
|
289
|
-
if (rowErrors.length > 0) {
|
|
290
|
-
errors.push(...rowErrors);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
if (primaryKeyColumn && row[primaryKeyColumn.name]) {
|
|
294
|
-
const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
|
|
295
|
-
.list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
|
|
296
|
-
if (existingRecord.length > 0) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
yield this.adminforth.createResourceRecord({
|
|
301
|
-
resource: resource,
|
|
302
|
-
record: row,
|
|
303
|
-
adminUser: adminUser,
|
|
304
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
305
|
-
});
|
|
306
|
-
importedCount++;
|
|
307
|
-
}
|
|
308
|
-
catch (e) {
|
|
309
|
-
errors.push(e.message);
|
|
310
|
-
}
|
|
311
|
-
}))));
|
|
312
|
-
return { ok: true, importedCount, errors };
|
|
191
|
+
return this.importCsvNewOnly(data, {
|
|
192
|
+
adminUser,
|
|
193
|
+
headers,
|
|
194
|
+
extra: { body, query, headers, cookies, requestUrl, response },
|
|
195
|
+
});
|
|
313
196
|
})
|
|
314
197
|
});
|
|
315
198
|
server.endpoint({
|
|
316
199
|
method: 'POST',
|
|
317
200
|
path: `/plugin/${this.pluginInstanceId}/check-records`,
|
|
318
201
|
request_schema: importCsvBodySchema,
|
|
319
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser
|
|
320
|
-
const
|
|
202
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
|
|
203
|
+
const { data } = body;
|
|
321
204
|
const access = yield this.ensureAnyAllowed(adminUser, [
|
|
322
205
|
{ source: ActionCheckSource.ListRequest, action: AllowedActionsEnum.list },
|
|
323
206
|
{ source: ActionCheckSource.ShowRequest, action: AllowedActionsEnum.show },
|
|
@@ -325,29 +208,198 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
325
208
|
if (!access.ok) {
|
|
326
209
|
return { ok: false, error: access.error };
|
|
327
210
|
}
|
|
328
|
-
|
|
329
|
-
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
330
|
-
const columns = this.getColumnNames(data);
|
|
331
|
-
const rows = this.buildRowsFromData(data, columns, undefined, { coerceTypes: false });
|
|
332
|
-
const primaryKeys = rows
|
|
333
|
-
.map(row => primaryKeyColumn ? row[primaryKeyColumn.name] : undefined)
|
|
334
|
-
.filter(key => key !== undefined && key !== null && key !== '');
|
|
335
|
-
const existingRecords = yield this.adminforth
|
|
336
|
-
.resource(this.resourceConfig.resourceId)
|
|
337
|
-
.list([{
|
|
338
|
-
field: primaryKeyColumn.name,
|
|
339
|
-
operator: AdminForthFilterOperators.IN,
|
|
340
|
-
value: primaryKeys,
|
|
341
|
-
}]);
|
|
342
|
-
return {
|
|
343
|
-
ok: true,
|
|
344
|
-
total: rows.length,
|
|
345
|
-
existingCount: existingRecords.length,
|
|
346
|
-
newCount: rows.length - existingRecords.length,
|
|
347
|
-
};
|
|
211
|
+
return this.checkRecords(data);
|
|
348
212
|
})
|
|
349
213
|
});
|
|
350
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
|
+
}
|
|
351
403
|
getColumnNames(data) {
|
|
352
404
|
return Object.keys(data !== null && data !== void 0 ? data : {});
|
|
353
405
|
}
|
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";
|
|
@@ -21,7 +21,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
21
21
|
adminforth: IAdminForth;
|
|
22
22
|
auditLogPlugin: Record<string, any> | undefined;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
constructor(options: PluginOptions) {
|
|
26
26
|
super(options, import.meta.url);
|
|
27
27
|
this.options = options;
|
|
@@ -136,9 +136,8 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
136
136
|
method: 'POST',
|
|
137
137
|
path: `/plugin/${this.pluginInstanceId}/export-csv`,
|
|
138
138
|
request_schema: exportCsvBodySchema,
|
|
139
|
-
handler: async ({ body, adminUser, headers
|
|
140
|
-
const
|
|
141
|
-
const { filters, sort, selectedIds } = payload;
|
|
139
|
+
handler: async ({ body, adminUser, headers }) => {
|
|
140
|
+
const { filters, sort, selectedIds } = body as z.infer<typeof exportCsvBodySchema>;
|
|
142
141
|
if (!filters || !sort) {
|
|
143
142
|
return { ok: false, error: 'Missing filters or sort in request body' };
|
|
144
143
|
}
|
|
@@ -171,44 +170,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
171
170
|
}];
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
|
|
175
|
-
resource: this.resourceConfig,
|
|
176
|
-
limit: 1e6,
|
|
177
|
-
offset: 0,
|
|
178
|
-
filters: this.adminforth.connectors[this.resourceConfig.dataSource].validateAndNormalizeInputFilters(effectiveFilters),
|
|
179
|
-
sort,
|
|
180
|
-
getTotals: true,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// prepare data for PapaParse unparse
|
|
184
|
-
const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
|
|
185
|
-
|
|
186
|
-
const columnsToForceQuote = columns.map(col => {
|
|
187
|
-
return col.type !== AdminForthDataTypes.FLOAT
|
|
188
|
-
&& col.type !== AdminForthDataTypes.INTEGER
|
|
189
|
-
&& col.type !== AdminForthDataTypes.BOOLEAN;
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const fields = columns.map((col) => col.name);
|
|
193
|
-
|
|
194
|
-
const rows = data.data.map((row) => {
|
|
195
|
-
return columns.map((col) => {
|
|
196
|
-
const value = row[col.name];
|
|
197
|
-
if (col.type === AdminForthDataTypes.JSON || col.isArray?.enabled) {
|
|
198
|
-
return value == null ? value : JSON.stringify(value);
|
|
199
|
-
}
|
|
200
|
-
return value;
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
this.tryToAuditLogAction('export', `Export CSV with filters: ${JSON.stringify(effectiveFilters)} and sort: ${JSON.stringify(sort)}. Total records: ${rows.length}`, adminUser, headers);
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
data: { fields, data: rows },
|
|
208
|
-
columnsToForceQuote,
|
|
209
|
-
exportedCount: data.total,
|
|
210
|
-
ok: true
|
|
211
|
-
};
|
|
173
|
+
return this.exportCsv(effectiveFilters, sort, { adminUser, headers });
|
|
212
174
|
}
|
|
213
175
|
});
|
|
214
176
|
|
|
@@ -217,8 +179,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
217
179
|
path: `/plugin/${this.pluginInstanceId}/import-csv`,
|
|
218
180
|
request_schema: importCsvBodySchema,
|
|
219
181
|
handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
|
|
220
|
-
const
|
|
221
|
-
const { data } = payload;
|
|
182
|
+
const { data } = body as z.infer<typeof importCsvBodySchema>;
|
|
222
183
|
if (!data || typeof data !== 'object') {
|
|
223
184
|
return { ok: false, error: 'Invalid data format. Expected an object with column names as keys and arrays of values as values.' };
|
|
224
185
|
}
|
|
@@ -233,66 +194,12 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
233
194
|
if (!createEditAccess.ok) {
|
|
234
195
|
return { ok: false, error: createEditAccess.error };
|
|
235
196
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
244
|
-
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
245
|
-
|
|
246
|
-
console.log('Prepared rows for import:', rows);
|
|
247
|
-
this.tryToAuditLogAction('import', `Import CSV with ${Object.keys(data).length} columns`, adminUser, headers);
|
|
248
|
-
|
|
249
|
-
let importedCount = 0;
|
|
250
|
-
let updatedCount = 0;
|
|
251
|
-
const limit = pLimit(100);
|
|
252
|
-
|
|
253
|
-
await Promise.all(rows.map((row) => limit(async () => {
|
|
254
|
-
try {
|
|
255
|
-
const rowErrors = await this.isRowValid(row);
|
|
256
|
-
if (rowErrors.length > 0) {
|
|
257
|
-
errors.push(...rowErrors);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const recordId = primaryKeyColumn ? row[primaryKeyColumn.name] as string : undefined;
|
|
261
|
-
if (primaryKeyColumn && recordId) {
|
|
262
|
-
const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
263
|
-
.list([Filters.EQ(primaryKeyColumn.name, recordId)]);
|
|
264
|
-
|
|
265
|
-
if (existingRecord.length > 0) {
|
|
266
|
-
const connector = this.adminforth.connectors[resource.dataSource];
|
|
267
|
-
const oldRecord = await connector.getRecordByPrimaryKey(resource, recordId)
|
|
268
|
-
if (!oldRecord) {
|
|
269
|
-
const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
|
|
270
|
-
return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
|
|
271
|
-
}
|
|
272
|
-
const { error } = await this.adminforth.updateResourceRecord({
|
|
273
|
-
resource, updates: row, adminUser, oldRecord, recordId, response,
|
|
274
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
275
|
-
});
|
|
276
|
-
if (error) {
|
|
277
|
-
return { error };
|
|
278
|
-
}
|
|
279
|
-
updatedCount++;
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
await this.adminforth.createResourceRecord({
|
|
284
|
-
resource: resource,
|
|
285
|
-
record: row,
|
|
286
|
-
adminUser: adminUser,
|
|
287
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
288
|
-
});
|
|
289
|
-
importedCount++;
|
|
290
|
-
} catch (e) {
|
|
291
|
-
errors.push(e.message);
|
|
292
|
-
}
|
|
293
|
-
})));
|
|
294
|
-
|
|
295
|
-
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
|
+
});
|
|
296
203
|
}
|
|
297
204
|
});
|
|
298
205
|
|
|
@@ -301,8 +208,7 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
301
208
|
path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
|
|
302
209
|
request_schema: importCsvBodySchema,
|
|
303
210
|
handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
|
|
304
|
-
const
|
|
305
|
-
const { data } = payload;
|
|
211
|
+
const { data } = body as z.infer<typeof importCsvBodySchema>;
|
|
306
212
|
if (!data || typeof data !== 'object') {
|
|
307
213
|
return { ok: false, error: 'Invalid data format. Expected an object with column names as keys and arrays of values as values.' };
|
|
308
214
|
}
|
|
@@ -314,48 +220,11 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
314
220
|
if (!access.ok) {
|
|
315
221
|
return { ok: false, error: access.error };
|
|
316
222
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
325
|
-
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
326
|
-
this.tryToAuditLogAction('import', `Import CSV (new only) with ${Object.keys(data).length} columns`, adminUser, headers);
|
|
327
|
-
|
|
328
|
-
let importedCount = 0;
|
|
329
|
-
const limit = pLimit(100);
|
|
330
|
-
|
|
331
|
-
await Promise.all(rows.map((row) => limit(async () => {
|
|
332
|
-
try {
|
|
333
|
-
const rowErrors = await this.isRowValid(row);
|
|
334
|
-
if (rowErrors.length > 0) {
|
|
335
|
-
errors.push(...rowErrors);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (primaryKeyColumn && row[primaryKeyColumn.name]) {
|
|
339
|
-
const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
340
|
-
.list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
|
|
341
|
-
|
|
342
|
-
if (existingRecord.length > 0) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
await this.adminforth.createResourceRecord({
|
|
347
|
-
resource: resource,
|
|
348
|
-
record: row,
|
|
349
|
-
adminUser: adminUser,
|
|
350
|
-
extra: { body, query, headers, cookies, requestUrl, response }
|
|
351
|
-
});
|
|
352
|
-
importedCount++;
|
|
353
|
-
} catch (e) {
|
|
354
|
-
errors.push(e.message);
|
|
355
|
-
}
|
|
356
|
-
})));
|
|
357
|
-
|
|
358
|
-
return { ok: true, importedCount, errors };
|
|
223
|
+
return this.importCsvNewOnly(data, {
|
|
224
|
+
adminUser,
|
|
225
|
+
headers,
|
|
226
|
+
extra: { body, query, headers, cookies, requestUrl, response },
|
|
227
|
+
});
|
|
359
228
|
}
|
|
360
229
|
});
|
|
361
230
|
|
|
@@ -363,8 +232,8 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
363
232
|
method: 'POST',
|
|
364
233
|
path: `/plugin/${this.pluginInstanceId}/check-records`,
|
|
365
234
|
request_schema: importCsvBodySchema,
|
|
366
|
-
handler: async ({ body, adminUser
|
|
367
|
-
const
|
|
235
|
+
handler: async ({ body, adminUser }) => {
|
|
236
|
+
const { data } = body as z.infer<typeof importCsvBodySchema>;
|
|
368
237
|
const access = await this.ensureAnyAllowed(
|
|
369
238
|
adminUser,
|
|
370
239
|
[
|
|
@@ -376,32 +245,240 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
376
245
|
if (!access.ok) {
|
|
377
246
|
return { ok: false, error: access.error };
|
|
378
247
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const primaryKeys = rows
|
|
385
|
-
.map(row => primaryKeyColumn ? row[primaryKeyColumn.name] : undefined)
|
|
386
|
-
.filter(key => key !== undefined && key !== null && key !== '');
|
|
387
|
-
|
|
388
|
-
const existingRecords = await this.adminforth
|
|
389
|
-
.resource(this.resourceConfig.resourceId)
|
|
390
|
-
.list([{
|
|
391
|
-
field: primaryKeyColumn.name,
|
|
392
|
-
operator: AdminForthFilterOperators.IN,
|
|
393
|
-
value: primaryKeys,
|
|
394
|
-
}]);
|
|
248
|
+
return this.checkRecords(data);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
395
252
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
+
});
|
|
402
277
|
|
|
403
|
-
|
|
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;
|
|
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
|
+
});
|
|
404
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
|
+
};
|
|
405
482
|
}
|
|
406
483
|
|
|
407
484
|
private getColumnNames(data: Record<string, unknown[]>): string[] {
|