@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/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthFilterOperators } from "adminforth";
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
- await this.adminforth.resource(this.resourceConfig.resourceId).create(row);
143
- importedCount++;
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
- errors.push(e.message);
164
+ errors.push(e.message);
146
165
  }
147
166
  }));
148
167
 
168
+ return { ok: true, importedCount, updatedCount, errors };
169
+ }
170
+ });
149
171
 
150
- return { ok: true, importedCount, errors};
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.2",
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/ && npm version patch",
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
- "keywords": [],
13
- "author": "",
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
  }