@adminforth/import-export 1.4.8 → 1.4.10
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/.woodpecker/release.yml +4 -4
- package/build.log +2 -2
- package/custom/ExportCsv.vue +2 -5
- package/custom/ImportCsv.vue +2 -6
- package/dist/custom/ExportCsv.vue +2 -5
- package/dist/custom/ImportCsv.vue +2 -6
- package/dist/index.js +80 -77
- package/index.ts +108 -86
- package/package.json +4 -4
package/.woodpecker/release.yml
CHANGED
|
@@ -17,18 +17,18 @@ steps:
|
|
|
17
17
|
- infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
|
|
18
18
|
|
|
19
19
|
build:
|
|
20
|
-
image:
|
|
20
|
+
image: devforth/node20-pnpm:latest
|
|
21
21
|
when:
|
|
22
22
|
- event: push
|
|
23
23
|
commands:
|
|
24
24
|
- apt update && apt install -y rsync
|
|
25
25
|
- . /woodpecker/deploy.vault.env
|
|
26
|
-
-
|
|
26
|
+
- pnpm install
|
|
27
27
|
- /bin/bash ./.woodpecker/buildRelease.sh
|
|
28
28
|
- npm audit signatures
|
|
29
29
|
|
|
30
30
|
release:
|
|
31
|
-
image:
|
|
31
|
+
image: devforth/node20-pnpm:latest
|
|
32
32
|
when:
|
|
33
33
|
- event:
|
|
34
34
|
- push
|
|
@@ -36,7 +36,7 @@ steps:
|
|
|
36
36
|
- main
|
|
37
37
|
commands:
|
|
38
38
|
- . /woodpecker/deploy.vault.env
|
|
39
|
-
-
|
|
39
|
+
- pnpm exec semantic-release
|
|
40
40
|
|
|
41
41
|
slack-on-failure:
|
|
42
42
|
image: curlimages/curl
|
package/build.log
CHANGED
|
@@ -10,5 +10,5 @@ custom/package-lock.json
|
|
|
10
10
|
custom/package.json
|
|
11
11
|
custom/tsconfig.json
|
|
12
12
|
|
|
13
|
-
sent 16,
|
|
14
|
-
total size is 16,
|
|
13
|
+
sent 16,704 bytes received 115 bytes 33,638.00 bytes/sec
|
|
14
|
+
total size is 16,284 speedup is 0.97
|
package/custom/ExportCsv.vue
CHANGED
|
@@ -54,12 +54,9 @@ async function exportCsv() {
|
|
|
54
54
|
throw new Error(resp.error);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Parse the CSV data to ensure proper formatting
|
|
58
|
-
const parsedData = Papa.parse(resp.data).data;
|
|
59
|
-
|
|
60
57
|
// Generate properly formatted CSV
|
|
61
|
-
const csvContent = '\ufeff' + Papa.unparse(
|
|
62
|
-
quotes:
|
|
58
|
+
const csvContent = '\ufeff' + Papa.unparse(resp.data, {
|
|
59
|
+
quotes: resp.columnsToForceQuote, // Force quotes only certain columns (!!!not all this breaks BI/Excel tasks)
|
|
63
60
|
quoteChar: '"',
|
|
64
61
|
escapeChar: '"',
|
|
65
62
|
});
|
package/custom/ImportCsv.vue
CHANGED
|
@@ -165,6 +165,7 @@ async function importCsv() {
|
|
|
165
165
|
Papa.parse(text, {
|
|
166
166
|
header: true,
|
|
167
167
|
skipEmptyLines: true,
|
|
168
|
+
// dynamicTyping: true, - bad option becaue it tries to parse "1" -> int even if the column is string
|
|
168
169
|
|
|
169
170
|
complete: async (results) => {
|
|
170
171
|
if (results.errors.length > 0) {
|
|
@@ -214,12 +215,7 @@ async function importCsv() {
|
|
|
214
215
|
reader.readAsText(file);
|
|
215
216
|
};
|
|
216
217
|
}
|
|
217
|
-
|
|
218
|
-
console.log('handleImportClick', checkProgress.value);
|
|
219
|
-
if (!checkProgress.value) {
|
|
220
|
-
importCsv();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
218
|
+
|
|
223
219
|
|
|
224
220
|
function click() {
|
|
225
221
|
importCsv();
|
|
@@ -54,12 +54,9 @@ async function exportCsv() {
|
|
|
54
54
|
throw new Error(resp.error);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Parse the CSV data to ensure proper formatting
|
|
58
|
-
const parsedData = Papa.parse(resp.data).data;
|
|
59
|
-
|
|
60
57
|
// Generate properly formatted CSV
|
|
61
|
-
const csvContent = '\ufeff' + Papa.unparse(
|
|
62
|
-
quotes:
|
|
58
|
+
const csvContent = '\ufeff' + Papa.unparse(resp.data, {
|
|
59
|
+
quotes: resp.columnsToForceQuote, // Force quotes only certain columns (!!!not all this breaks BI/Excel tasks)
|
|
63
60
|
quoteChar: '"',
|
|
64
61
|
escapeChar: '"',
|
|
65
62
|
});
|
|
@@ -165,6 +165,7 @@ async function importCsv() {
|
|
|
165
165
|
Papa.parse(text, {
|
|
166
166
|
header: true,
|
|
167
167
|
skipEmptyLines: true,
|
|
168
|
+
// dynamicTyping: true, - bad option becaue it tries to parse "1" -> int even if the column is string
|
|
168
169
|
|
|
169
170
|
complete: async (results) => {
|
|
170
171
|
if (results.errors.length > 0) {
|
|
@@ -214,12 +215,7 @@ async function importCsv() {
|
|
|
214
215
|
reader.readAsText(file);
|
|
215
216
|
};
|
|
216
217
|
}
|
|
217
|
-
|
|
218
|
-
console.log('handleImportClick', checkProgress.value);
|
|
219
|
-
if (!checkProgress.value) {
|
|
220
|
-
importCsv();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
218
|
+
|
|
223
219
|
|
|
224
220
|
function click() {
|
|
225
221
|
importCsv();
|
package/dist/index.js
CHANGED
|
@@ -64,39 +64,23 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
64
64
|
sort,
|
|
65
65
|
getTotals: true,
|
|
66
66
|
});
|
|
67
|
-
//
|
|
67
|
+
// prepare data for PapaParse unparse
|
|
68
68
|
const columns = this.resourceConfig.columns.filter((col) => !col.virtual);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
else if (value === false) {
|
|
84
|
-
return 'false';
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const str = String(value);
|
|
88
|
-
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
89
|
-
return `"${str.replace(/"/g, '""')}"`;
|
|
90
|
-
}
|
|
91
|
-
return `"${str}"`;
|
|
69
|
+
const columnsToForceQuote = columns.map(col => {
|
|
70
|
+
return col.type !== AdminForthDataTypes.FLOAT
|
|
71
|
+
&& col.type !== AdminForthDataTypes.INTEGER
|
|
72
|
+
&& col.type !== AdminForthDataTypes.BOOLEAN;
|
|
73
|
+
});
|
|
74
|
+
const fields = columns.map((col) => col.name);
|
|
75
|
+
const rows = data.data.map((row) => {
|
|
76
|
+
return columns.map((col) => row[col.name]);
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
data: { fields, data: rows },
|
|
80
|
+
columnsToForceQuote,
|
|
81
|
+
exportedCount: data.total,
|
|
82
|
+
ok: true
|
|
92
83
|
};
|
|
93
|
-
let csv = data.data.map((row) => {
|
|
94
|
-
return columns.map((col) => escapeCSV(row[col.name], col)).join(',');
|
|
95
|
-
}).join('\n');
|
|
96
|
-
// add headers
|
|
97
|
-
const headers = columns.map((col) => escapeCSV(col.name)).join(',');
|
|
98
|
-
csv = `${headers}\n${csv}`;
|
|
99
|
-
return { data: csv, exportedCount: data.total, ok: true };
|
|
100
84
|
})
|
|
101
85
|
});
|
|
102
86
|
server.endpoint({
|
|
@@ -105,28 +89,14 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
105
89
|
noAuth: true,
|
|
106
90
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
|
|
107
91
|
const { data } = body;
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
// check column names are valid
|
|
111
|
-
const errors = [];
|
|
112
|
-
columns.forEach((col) => {
|
|
113
|
-
if (!this.resourceConfig.columns.some((c) => c.name === col)) {
|
|
114
|
-
const similar = suggestIfTypo(this.resourceConfig.columns.map((c) => c.name), col);
|
|
115
|
-
errors.push(`Column '${col}' defined in CSV not found in resource '${this.resourceConfig.resourceId}'. ${similar ? `If you mean '${similar}', rename it in CSV` : 'If column is in database but not in resource configuration, add it with showIn:[]'}`);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
92
|
+
const columns = this.getColumnNames(data);
|
|
93
|
+
const { errors, resourceColumns } = this.validateColumns(columns);
|
|
118
94
|
if (errors.length > 0) {
|
|
119
95
|
return { ok: false, errors };
|
|
120
96
|
}
|
|
121
97
|
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
const row = {};
|
|
125
|
-
for (let j = 0; j < columns.length; j++) {
|
|
126
|
-
row[columns[j]] = columnValues[j][i];
|
|
127
|
-
}
|
|
128
|
-
rows.push(row);
|
|
129
|
-
}
|
|
98
|
+
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
99
|
+
console.log('Prepared rows for import:', rows);
|
|
130
100
|
let importedCount = 0;
|
|
131
101
|
let updatedCount = 0;
|
|
132
102
|
yield Promise.all(rows.map((row) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -157,28 +127,13 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
157
127
|
noAuth: true,
|
|
158
128
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
|
|
159
129
|
const { data } = body;
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
// check column names are valid
|
|
163
|
-
const errors = [];
|
|
164
|
-
columns.forEach((col) => {
|
|
165
|
-
if (!this.resourceConfig.columns.some((c) => c.name === col)) {
|
|
166
|
-
const similar = suggestIfTypo(this.resourceConfig.columns.map((c) => c.name), col);
|
|
167
|
-
errors.push(`Column '${col}' defined in CSV not found in resource '${this.resourceConfig.resourceId}'. ${similar ? `If you mean '${similar}', rename it in CSV` : 'If column is in database but not in resource configuration, add it with showIn:[]'}`);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
130
|
+
const columns = this.getColumnNames(data);
|
|
131
|
+
const { errors, resourceColumns } = this.validateColumns(columns);
|
|
170
132
|
if (errors.length > 0) {
|
|
171
133
|
return { ok: false, errors };
|
|
172
134
|
}
|
|
173
135
|
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
174
|
-
const
|
|
175
|
-
for (let i = 0; i < columnValues[0].length; i++) {
|
|
176
|
-
const row = {};
|
|
177
|
-
for (let j = 0; j < columns.length; j++) {
|
|
178
|
-
row[columns[j]] = columnValues[j][i];
|
|
179
|
-
}
|
|
180
|
-
rows.push(row);
|
|
181
|
-
}
|
|
136
|
+
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
182
137
|
let importedCount = 0;
|
|
183
138
|
yield Promise.all(rows.map((row) => __awaiter(this, void 0, void 0, function* () {
|
|
184
139
|
try {
|
|
@@ -206,17 +161,10 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
206
161
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
|
|
207
162
|
const { data } = body;
|
|
208
163
|
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
209
|
-
const columns =
|
|
210
|
-
const
|
|
211
|
-
const rows = Array.from({ length: columnValues[0].length }, (_, i) => {
|
|
212
|
-
const row = {};
|
|
213
|
-
for (let j = 0; j < columns.length; j++) {
|
|
214
|
-
row[columns[j]] = columnValues[j][i];
|
|
215
|
-
}
|
|
216
|
-
return row;
|
|
217
|
-
});
|
|
164
|
+
const columns = this.getColumnNames(data);
|
|
165
|
+
const rows = this.buildRowsFromData(data, columns, undefined, { coerceTypes: false });
|
|
218
166
|
const primaryKeys = rows
|
|
219
|
-
.map(row => row[primaryKeyColumn.name])
|
|
167
|
+
.map(row => primaryKeyColumn ? row[primaryKeyColumn.name] : undefined)
|
|
220
168
|
.filter(key => key !== undefined && key !== null && key !== '');
|
|
221
169
|
const existingRecords = yield this.adminforth
|
|
222
170
|
.resource(this.resourceConfig.resourceId)
|
|
@@ -234,4 +182,59 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
234
182
|
})
|
|
235
183
|
});
|
|
236
184
|
}
|
|
185
|
+
getColumnNames(data) {
|
|
186
|
+
return Object.keys(data !== null && data !== void 0 ? data : {});
|
|
187
|
+
}
|
|
188
|
+
validateColumns(columns) {
|
|
189
|
+
const errors = [];
|
|
190
|
+
const resourceColumns = [];
|
|
191
|
+
columns.forEach((col) => {
|
|
192
|
+
const resourceColumn = this.resourceConfig.columns.find((c) => c.name === col);
|
|
193
|
+
if (!resourceColumn) {
|
|
194
|
+
const similar = suggestIfTypo(this.resourceConfig.columns.map((c) => c.name), col);
|
|
195
|
+
errors.push(`Column '${col}' defined in CSV not found in resource '${this.resourceConfig.resourceId}'. ${similar
|
|
196
|
+
? `If you mean '${similar}', rename it in CSV`
|
|
197
|
+
: 'If column is in database but not in resource configuration, add it with showIn:[]'}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
resourceColumns.push(resourceColumn);
|
|
201
|
+
});
|
|
202
|
+
return { errors, resourceColumns };
|
|
203
|
+
}
|
|
204
|
+
buildRowsFromData(data, columns, resourceColumns, { coerceTypes } = { coerceTypes: true }) {
|
|
205
|
+
const columnValues = Object.values(data !== null && data !== void 0 ? data : {});
|
|
206
|
+
if (columns.length === 0 || columnValues.length === 0) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const rows = [];
|
|
210
|
+
const rowCount = columnValues[0].length;
|
|
211
|
+
for (let i = 0; i < rowCount; i++) {
|
|
212
|
+
const row = {};
|
|
213
|
+
for (let j = 0; j < columns.length; j++) {
|
|
214
|
+
const val = columnValues[j][i];
|
|
215
|
+
const resourceCol = resourceColumns ? resourceColumns[j] : undefined;
|
|
216
|
+
row[columns[j]] = coerceTypes
|
|
217
|
+
? this.coerceValue(resourceCol, val)
|
|
218
|
+
: val;
|
|
219
|
+
}
|
|
220
|
+
rows.push(row);
|
|
221
|
+
}
|
|
222
|
+
return rows;
|
|
223
|
+
}
|
|
224
|
+
coerceValue(resourceCol, val) {
|
|
225
|
+
if (!resourceCol || val === '') {
|
|
226
|
+
return val;
|
|
227
|
+
}
|
|
228
|
+
if ((resourceCol.type === AdminForthDataTypes.INTEGER
|
|
229
|
+
|| resourceCol.type === AdminForthDataTypes.FLOAT)) {
|
|
230
|
+
return +val;
|
|
231
|
+
}
|
|
232
|
+
if (resourceCol.type === AdminForthDataTypes.BOOLEAN) {
|
|
233
|
+
if (typeof val === 'string') {
|
|
234
|
+
return val.toLowerCase() === 'true' || val === '1';
|
|
235
|
+
}
|
|
236
|
+
return val === 1 || val === true;
|
|
237
|
+
}
|
|
238
|
+
return val;
|
|
239
|
+
}
|
|
237
240
|
}
|
package/index.ts
CHANGED
|
@@ -66,42 +66,27 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
66
66
|
getTotals: true,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
//
|
|
69
|
+
// prepare data for PapaParse unparse
|
|
70
70
|
const columns = this.resourceConfig.columns.filter((col) => !col.virtual);
|
|
71
|
-
|
|
72
|
-
const escapeCSV = (value: any, column?: AdminForthResourceColumn) => {
|
|
73
|
-
if (value === null || value === undefined) {
|
|
74
|
-
return '""';
|
|
75
|
-
}
|
|
76
|
-
if (column?.type === AdminForthDataTypes.FLOAT
|
|
77
|
-
|| column?.type === AdminForthDataTypes.INTEGER) {
|
|
78
|
-
// no quotes for numbers
|
|
79
|
-
return String(value);
|
|
80
|
-
}
|
|
81
|
-
if (column?.type === AdminForthDataTypes.BOOLEAN) {
|
|
82
|
-
// no quotes for boolean values
|
|
83
|
-
if (value === true) {
|
|
84
|
-
return 'true';
|
|
85
|
-
} else if (value === false) {
|
|
86
|
-
return 'false';
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
const str = String(value);
|
|
90
|
-
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
91
|
-
return `"${str.replace(/"/g, '""')}"`;
|
|
92
|
-
}
|
|
93
|
-
return `"${str}"`;
|
|
94
|
-
};
|
|
95
71
|
|
|
96
|
-
|
|
97
|
-
return
|
|
98
|
-
|
|
72
|
+
const columnsToForceQuote = columns.map(col => {
|
|
73
|
+
return col.type !== AdminForthDataTypes.FLOAT
|
|
74
|
+
&& col.type !== AdminForthDataTypes.INTEGER
|
|
75
|
+
&& col.type !== AdminForthDataTypes.BOOLEAN;
|
|
76
|
+
})
|
|
99
77
|
|
|
100
|
-
|
|
101
|
-
const headers = columns.map((col) => escapeCSV(col.name)).join(',');
|
|
102
|
-
csv = `${headers}\n${csv}`;
|
|
78
|
+
const fields = columns.map((col) => col.name);
|
|
103
79
|
|
|
104
|
-
|
|
80
|
+
const rows = data.data.map((row) => {
|
|
81
|
+
return columns.map((col) => row[col.name]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
data: { fields, data: rows },
|
|
86
|
+
columnsToForceQuote,
|
|
87
|
+
exportedCount: data.total,
|
|
88
|
+
ok: true
|
|
89
|
+
};
|
|
105
90
|
}
|
|
106
91
|
});
|
|
107
92
|
|
|
@@ -111,33 +96,15 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
111
96
|
noAuth: true,
|
|
112
97
|
handler: async ({ body }) => {
|
|
113
98
|
const { data } = body;
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
// check column names are valid
|
|
118
|
-
const errors: string[] = [];
|
|
119
|
-
columns.forEach((col) => {
|
|
120
|
-
if (!this.resourceConfig.columns.some((c) => c.name === col)) {
|
|
121
|
-
const similar = suggestIfTypo(this.resourceConfig.columns.map((c) => c.name), col);
|
|
122
|
-
errors.push(`Column '${col}' defined in CSV not found in resource '${this.resourceConfig.resourceId}'. ${
|
|
123
|
-
similar ? `If you mean '${similar}', rename it in CSV` : 'If column is in database but not in resource configuration, add it with showIn:[]'}`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
99
|
+
const columns = this.getColumnNames(data);
|
|
100
|
+
const { errors, resourceColumns } = this.validateColumns(columns);
|
|
127
101
|
if (errors.length > 0) {
|
|
128
102
|
return { ok: false, errors };
|
|
129
103
|
}
|
|
130
|
-
|
|
131
104
|
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
105
|
+
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
132
106
|
|
|
133
|
-
|
|
134
|
-
for (let i = 0; i < columnValues[0].length; i++) {
|
|
135
|
-
const row = {};
|
|
136
|
-
for (let j = 0; j < columns.length; j++) {
|
|
137
|
-
row[columns[j]] = columnValues[j][i];
|
|
138
|
-
}
|
|
139
|
-
rows.push(row);
|
|
140
|
-
}
|
|
107
|
+
console.log('Prepared rows for import:', rows);
|
|
141
108
|
|
|
142
109
|
let importedCount = 0;
|
|
143
110
|
let updatedCount = 0;
|
|
@@ -172,32 +139,14 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
172
139
|
noAuth: true,
|
|
173
140
|
handler: async ({ body }) => {
|
|
174
141
|
const { data } = body;
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
// check column names are valid
|
|
179
|
-
const errors: string[] = [];
|
|
180
|
-
columns.forEach((col) => {
|
|
181
|
-
if (!this.resourceConfig.columns.some((c) => c.name === col)) {
|
|
182
|
-
const similar = suggestIfTypo(this.resourceConfig.columns.map((c) => c.name), col);
|
|
183
|
-
errors.push(`Column '${col}' defined in CSV not found in resource '${this.resourceConfig.resourceId}'. ${
|
|
184
|
-
similar ? `If you mean '${similar}', rename it in CSV` : 'If column is in database but not in resource configuration, add it with showIn:[]'}`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
142
|
+
const columns = this.getColumnNames(data);
|
|
143
|
+
const { errors, resourceColumns } = this.validateColumns(columns);
|
|
188
144
|
if (errors.length > 0) {
|
|
189
145
|
return { ok: false, errors };
|
|
190
146
|
}
|
|
191
147
|
|
|
192
148
|
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
193
|
-
const
|
|
194
|
-
for (let i = 0; i < columnValues[0].length; i++) {
|
|
195
|
-
const row = {};
|
|
196
|
-
for (let j = 0; j < columns.length; j++) {
|
|
197
|
-
row[columns[j]] = columnValues[j][i];
|
|
198
|
-
}
|
|
199
|
-
rows.push(row);
|
|
200
|
-
}
|
|
149
|
+
const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
|
|
201
150
|
|
|
202
151
|
let importedCount = 0;
|
|
203
152
|
|
|
@@ -229,19 +178,11 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
229
178
|
handler: async ({ body }) => {
|
|
230
179
|
const { data } = body as { data: Record<string, unknown[]> };
|
|
231
180
|
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
232
|
-
const columns =
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const rows = Array.from({ length: columnValues[0].length }, (_, i) => {
|
|
236
|
-
const row = {};
|
|
237
|
-
for (let j = 0; j < columns.length; j++) {
|
|
238
|
-
row[columns[j]] = columnValues[j][i];
|
|
239
|
-
}
|
|
240
|
-
return row;
|
|
241
|
-
});
|
|
181
|
+
const columns = this.getColumnNames(data);
|
|
182
|
+
const rows = this.buildRowsFromData(data, columns, undefined, { coerceTypes: false });
|
|
242
183
|
|
|
243
184
|
const primaryKeys = rows
|
|
244
|
-
.map(row => row[primaryKeyColumn.name])
|
|
185
|
+
.map(row => primaryKeyColumn ? row[primaryKeyColumn.name] : undefined)
|
|
245
186
|
.filter(key => key !== undefined && key !== null && key !== '');
|
|
246
187
|
|
|
247
188
|
const existingRecords = await this.adminforth
|
|
@@ -263,4 +204,85 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
263
204
|
});
|
|
264
205
|
}
|
|
265
206
|
|
|
207
|
+
private getColumnNames(data: Record<string, unknown[]>): string[] {
|
|
208
|
+
return Object.keys(data ?? {});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private validateColumns(columns: string[]): {
|
|
212
|
+
errors: string[];
|
|
213
|
+
resourceColumns: AdminForthResourceColumn[];
|
|
214
|
+
} {
|
|
215
|
+
const errors: string[] = [];
|
|
216
|
+
const resourceColumns: AdminForthResourceColumn[] = [];
|
|
217
|
+
|
|
218
|
+
columns.forEach((col) => {
|
|
219
|
+
const resourceColumn = this.resourceConfig.columns.find((c) => c.name === col);
|
|
220
|
+
if (!resourceColumn) {
|
|
221
|
+
const similar = suggestIfTypo(this.resourceConfig.columns.map((c) => c.name), col);
|
|
222
|
+
errors.push(
|
|
223
|
+
`Column '${col}' defined in CSV not found in resource '${this.resourceConfig.resourceId}'. ${
|
|
224
|
+
similar
|
|
225
|
+
? `If you mean '${similar}', rename it in CSV`
|
|
226
|
+
: 'If column is in database but not in resource configuration, add it with showIn:[]'
|
|
227
|
+
}`
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
resourceColumns.push(resourceColumn);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return { errors, resourceColumns };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private buildRowsFromData(
|
|
238
|
+
data: Record<string, unknown[]>,
|
|
239
|
+
columns: string[],
|
|
240
|
+
resourceColumns?: AdminForthResourceColumn[],
|
|
241
|
+
{ coerceTypes }: { coerceTypes: boolean } = { coerceTypes: true }
|
|
242
|
+
) {
|
|
243
|
+
const columnValues: unknown[][] = Object.values(data ?? {});
|
|
244
|
+
if (columns.length === 0 || columnValues.length === 0) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const rows: Record<string, unknown>[] = [];
|
|
249
|
+
const rowCount = columnValues[0].length;
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < rowCount; i++) {
|
|
252
|
+
const row: Record<string, unknown> = {};
|
|
253
|
+
for (let j = 0; j < columns.length; j++) {
|
|
254
|
+
const val = columnValues[j][i];
|
|
255
|
+
const resourceCol = resourceColumns ? resourceColumns[j] : undefined;
|
|
256
|
+
row[columns[j]] = coerceTypes
|
|
257
|
+
? this.coerceValue(resourceCol, val)
|
|
258
|
+
: val;
|
|
259
|
+
}
|
|
260
|
+
rows.push(row);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return rows;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private coerceValue(resourceCol: AdminForthResourceColumn | undefined, val: unknown): unknown {
|
|
267
|
+
if (!resourceCol || val === '') {
|
|
268
|
+
return val;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (
|
|
272
|
+
(resourceCol.type === AdminForthDataTypes.INTEGER
|
|
273
|
+
|| resourceCol.type === AdminForthDataTypes.FLOAT)
|
|
274
|
+
) {
|
|
275
|
+
return +val;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (resourceCol.type === AdminForthDataTypes.BOOLEAN) {
|
|
279
|
+
if (typeof val === 'string') {
|
|
280
|
+
return val.toLowerCase() === 'true' || val === '1';
|
|
281
|
+
}
|
|
282
|
+
return val === 1 || val === true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return val;
|
|
286
|
+
}
|
|
287
|
+
|
|
266
288
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/import-export",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.10",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://adminforth.dev/docs/tutorial/Plugins/import-export/",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
|
|
13
|
-
"prepare": "npm link adminforth"
|
|
12
|
+
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
|
|
14
13
|
},
|
|
15
14
|
"repository": {
|
|
16
15
|
"type": "git",
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
"license": "ISC",
|
|
27
26
|
"description": "CSV import/export plugin for adminforth",
|
|
28
27
|
"peerDependencies": {
|
|
29
|
-
"adminforth": "
|
|
28
|
+
"adminforth": "^2.24.0"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
32
31
|
"@types/node": "^22.10.7",
|
|
@@ -43,6 +42,7 @@
|
|
|
43
42
|
[
|
|
44
43
|
"semantic-release-slack-bot",
|
|
45
44
|
{
|
|
45
|
+
"packageName": "@adminforth/import-export",
|
|
46
46
|
"notifyOnSuccess": true,
|
|
47
47
|
"notifyOnFail": true,
|
|
48
48
|
"slackIcon": ":package:",
|