@adminforth/import-export 1.0.2 → 1.2.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/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +44 -0
- package/.woodpecker/release.yml +44 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/build.log +14 -0
- package/custom/ExportCsv.vue +45 -37
- package/custom/ImportCsv.vue +116 -40
- package/custom/package-lock.json +47 -0
- package/custom/package.json +16 -0
- package/custom/tsconfig.json +19 -0
- package/dist/custom/ExportCsv.vue +45 -37
- package/dist/custom/ImportCsv.vue +116 -40
- package/dist/custom/package-lock.json +47 -0
- package/dist/custom/package.json +16 -0
- package/dist/custom/tsconfig.json +19 -0
- package/dist/index.js +103 -8
- package/index.ts +122 -13
- package/package.json +46 -7
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AdminForthPlugin, suggestIfTypo, AdminForthFilterOperators, Filters } from "adminforth";
|
|
2
2
|
import type { IAdminForth, IHttpServer, AdminForthResourceColumn, AdminForthComponentDeclaration, AdminForthResource } from "adminforth";
|
|
3
3
|
import type { PluginOptions } from './types.js';
|
|
4
4
|
|
|
@@ -85,20 +85,26 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
// csv export
|
|
88
|
-
|
|
89
88
|
const columns = this.resourceConfig.columns.filter((col) => !col.virtual);
|
|
89
|
+
|
|
90
|
+
const escapeCSV = (value: any) => {
|
|
91
|
+
if (value === null || value === undefined) return '""';
|
|
92
|
+
const str = String(value);
|
|
93
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
94
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
95
|
+
}
|
|
96
|
+
return `"${str}"`;
|
|
97
|
+
};
|
|
98
|
+
|
|
90
99
|
let csv = data.data.map((row) => {
|
|
91
|
-
return columns.map((col) =>
|
|
92
|
-
return row[col.name];
|
|
93
|
-
}).join(',');
|
|
100
|
+
return columns.map((col) => escapeCSV(row[col.name])).join(',');
|
|
94
101
|
}).join('\n');
|
|
95
102
|
|
|
96
103
|
// add headers
|
|
97
|
-
const headers = columns.map((col) => col.name).join(',');
|
|
104
|
+
const headers = columns.map((col) => escapeCSV(col.name)).join(',');
|
|
98
105
|
csv = `${headers}\n${csv}`;
|
|
99
106
|
|
|
100
107
|
return { data: csv, exportedCount: data.total, ok: true };
|
|
101
|
-
|
|
102
108
|
}
|
|
103
109
|
});
|
|
104
110
|
|
|
@@ -108,8 +114,6 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
108
114
|
noAuth: true,
|
|
109
115
|
handler: async ({ body }) => {
|
|
110
116
|
const { data } = body;
|
|
111
|
-
// data is in format {[columnName]: [value1, value2, value3...], [columnName2]: [value1, value2, value3...]}
|
|
112
|
-
// we need to convert it to [{columnName: value1, columnName2: value1}, {columnName: value2, columnName2: value2}...]
|
|
113
117
|
const rows = [];
|
|
114
118
|
const columns = Object.keys(data);
|
|
115
119
|
|
|
@@ -127,6 +131,8 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
127
131
|
return { ok: false, errors };
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
135
|
+
|
|
130
136
|
const columnValues: any[] = Object.values(data);
|
|
131
137
|
for (let i = 0; i < columnValues[0].length; i++) {
|
|
132
138
|
const row = {};
|
|
@@ -137,21 +143,124 @@ export default class ImportExport extends AdminForthPlugin {
|
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
let importedCount = 0;
|
|
146
|
+
let updatedCount = 0;
|
|
147
|
+
|
|
140
148
|
await Promise.all(rows.map(async (row) => {
|
|
141
149
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
if (primaryKeyColumn && row[primaryKeyColumn.name]) {
|
|
151
|
+
const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
152
|
+
.list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
|
|
153
|
+
|
|
154
|
+
if (existingRecord.length > 0) {
|
|
155
|
+
await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
156
|
+
.update(row[primaryKeyColumn.name], row);
|
|
157
|
+
updatedCount++;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await this.adminforth.resource(this.resourceConfig.resourceId).create(row);
|
|
162
|
+
importedCount++;
|
|
144
163
|
} catch (e) {
|
|
145
|
-
|
|
164
|
+
errors.push(e.message);
|
|
146
165
|
}
|
|
147
166
|
}));
|
|
148
167
|
|
|
168
|
+
return { ok: true, importedCount, updatedCount, errors };
|
|
169
|
+
}
|
|
170
|
+
});
|
|
149
171
|
|
|
150
|
-
|
|
172
|
+
server.endpoint({
|
|
173
|
+
method: 'POST',
|
|
174
|
+
path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
|
|
175
|
+
noAuth: true,
|
|
176
|
+
handler: async ({ body }) => {
|
|
177
|
+
const { data } = body;
|
|
178
|
+
const rows = [];
|
|
179
|
+
const columns = Object.keys(data);
|
|
151
180
|
|
|
181
|
+
// check column names are valid
|
|
182
|
+
const errors: string[] = [];
|
|
183
|
+
columns.forEach((col) => {
|
|
184
|
+
if (!this.resourceConfig.columns.some((c) => c.name === col)) {
|
|
185
|
+
const similar = suggestIfTypo(this.resourceConfig.columns.map((c) => c.name), col);
|
|
186
|
+
errors.push(`Column '${col}' defined in CSV not found in resource '${this.resourceConfig.resourceId}'. ${
|
|
187
|
+
similar ? `If you mean '${similar}', rename it in CSV` : 'If column is in database but not in resource configuration, add it with showIn:[]'}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (errors.length > 0) {
|
|
192
|
+
return { ok: false, errors };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
196
|
+
const columnValues: any[] = Object.values(data);
|
|
197
|
+
for (let i = 0; i < columnValues[0].length; i++) {
|
|
198
|
+
const row = {};
|
|
199
|
+
for (let j = 0; j < columns.length; j++) {
|
|
200
|
+
row[columns[j]] = columnValues[j][i];
|
|
201
|
+
}
|
|
202
|
+
rows.push(row);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let importedCount = 0;
|
|
206
|
+
|
|
207
|
+
await Promise.all(rows.map(async (row) => {
|
|
208
|
+
try {
|
|
209
|
+
if (primaryKeyColumn && row[primaryKeyColumn.name]) {
|
|
210
|
+
const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
|
|
211
|
+
.list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
|
|
212
|
+
|
|
213
|
+
if (existingRecord.length > 0) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
await this.adminforth.resource(this.resourceConfig.resourceId).create(row);
|
|
218
|
+
importedCount++;
|
|
219
|
+
} catch (e) {
|
|
220
|
+
errors.push(e.message);
|
|
221
|
+
}
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
return { ok: true, importedCount, errors };
|
|
152
225
|
}
|
|
153
226
|
});
|
|
154
227
|
|
|
228
|
+
server.endpoint({
|
|
229
|
+
method: 'POST',
|
|
230
|
+
path: `/plugin/${this.pluginInstanceId}/check-records`,
|
|
231
|
+
noAuth: true,
|
|
232
|
+
handler: async ({ body }) => {
|
|
233
|
+
const { data } = body;
|
|
234
|
+
|
|
235
|
+
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
|
|
236
|
+
|
|
237
|
+
const rows = [];
|
|
238
|
+
const columns = Object.keys(data);
|
|
239
|
+
const columnValues = Object.values(data);
|
|
240
|
+
for (let i = 0; i < (columnValues[0] as any[]).length; i++) {
|
|
241
|
+
const row = {};
|
|
242
|
+
for (let j = 0; j < columns.length; j++) {
|
|
243
|
+
row[columns[j]] = columnValues[j][i];
|
|
244
|
+
}
|
|
245
|
+
rows.push(row);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const primaryKeys = rows
|
|
249
|
+
.map(row => row[primaryKeyColumn.name])
|
|
250
|
+
.filter(key => key !== undefined && key !== null && key !== '');
|
|
251
|
+
|
|
252
|
+
const records = await this.adminforth.resource(this.resourceConfig.resourceId).list([])
|
|
253
|
+
|
|
254
|
+
const existingRecords = records.filter((record) => primaryKeys.includes(record[primaryKeyColumn.name]));
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
total: rows.length,
|
|
259
|
+
existingCount: existingRecords.length,
|
|
260
|
+
newCount: rows.length - existingRecords.length
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
});
|
|
155
264
|
}
|
|
156
265
|
|
|
157
266
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/import-export",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"homepage": "https://adminforth.dev/docs/tutorial/Plugins/import-export/",
|
|
7
8
|
"scripts": {
|
|
8
|
-
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/
|
|
9
|
-
"rollout": "npm run build && npm version patch && npm publish --access public",
|
|
9
|
+
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/",
|
|
10
10
|
"prepare": "npm link adminforth"
|
|
11
11
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/devforth/adminforth-import-export.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"adminforth",
|
|
18
|
+
"import",
|
|
19
|
+
"export",
|
|
20
|
+
"csv"
|
|
21
|
+
],
|
|
22
|
+
"author": "devforth",
|
|
14
23
|
"license": "ISC",
|
|
15
|
-
"description": "",
|
|
16
|
-
"dependencies": {}
|
|
24
|
+
"description": "CSV import/export plugin for adminforth",
|
|
25
|
+
"dependencies": {},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.10.7",
|
|
28
|
+
"semantic-release": "^24.2.1",
|
|
29
|
+
"semantic-release-slack-bot": "^4.0.2",
|
|
30
|
+
"typescript": "^5.7.3"
|
|
31
|
+
},
|
|
32
|
+
"release": {
|
|
33
|
+
"plugins": [
|
|
34
|
+
"@semantic-release/commit-analyzer",
|
|
35
|
+
"@semantic-release/release-notes-generator",
|
|
36
|
+
"@semantic-release/npm",
|
|
37
|
+
"@semantic-release/github",
|
|
38
|
+
[
|
|
39
|
+
"semantic-release-slack-bot",
|
|
40
|
+
{
|
|
41
|
+
"notifyOnSuccess": true,
|
|
42
|
+
"notifyOnFail": true,
|
|
43
|
+
"slackIcon": ":package:",
|
|
44
|
+
"markdownReleaseNotes": true
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
],
|
|
48
|
+
"branches": [
|
|
49
|
+
"main",
|
|
50
|
+
{
|
|
51
|
+
"name": "next",
|
|
52
|
+
"prerelease": true
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
17
56
|
}
|