@adminforth/import-export 1.4.25 → 1.4.27

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 CHANGED
@@ -11,5 +11,5 @@ custom/package.json
11
11
  custom/pnpm-lock.yaml
12
12
  custom/tsconfig.json
13
13
 
14
- sent 17,884 bytes received 134 bytes 36,036.00 bytes/sec
15
- total size is 17,395 speedup is 0.97
14
+ sent 17,871 bytes received 134 bytes 36,010.00 bytes/sec
15
+ total size is 17,389 speedup is 0.97
@@ -116,7 +116,7 @@ async function postData(data: Record<string, string[]>, skipDuplicates: boolean
116
116
  adminforth.list.refresh();
117
117
  }
118
118
  adminforth.alert({
119
- message: `Imported ${resp.importedCount || 0} records. Updated ${resp.updatedCount || 0} records. ${resp.errors?.length ? `Errors: ${resp.errors.join(', ')}` : ''}`,
119
+ message: `Imported ${resp.importedCount || 0} records. Updated ${resp.updatedCount || 0} records. ${resp.errors?.length ? `First error: ${resp.errors[0]}` : ''}`,
120
120
  variant: resp.errors?.length ? (
121
121
  resp.importedCount ? 'warning' : 'danger'
122
122
  ) : 'success'
@@ -138,7 +138,7 @@ async function postDataNewOnly(data: Record<string, string[]>) {
138
138
  adminforth.list.refresh();
139
139
  }
140
140
  adminforth.alert({
141
- message: `Imported ${resp.importedCount || 0} records. ${resp.errors?.length ? `Errors: ${resp.errors.join(', ')}` : ''}`,
141
+ message: `Imported ${resp.importedCount || 0} records. ${resp.errors?.length ? `First error: ${resp.errors[0]}` : ''}`,
142
142
  variant: resp.errors?.length ? 'warning' : 'success'
143
143
  });
144
144
 
@@ -116,7 +116,7 @@ async function postData(data: Record<string, string[]>, skipDuplicates: boolean
116
116
  adminforth.list.refresh();
117
117
  }
118
118
  adminforth.alert({
119
- message: `Imported ${resp.importedCount || 0} records. Updated ${resp.updatedCount || 0} records. ${resp.errors?.length ? `Errors: ${resp.errors.join(', ')}` : ''}`,
119
+ message: `Imported ${resp.importedCount || 0} records. Updated ${resp.updatedCount || 0} records. ${resp.errors?.length ? `First error: ${resp.errors[0]}` : ''}`,
120
120
  variant: resp.errors?.length ? (
121
121
  resp.importedCount ? 'warning' : 'danger'
122
122
  ) : 'success'
@@ -138,7 +138,7 @@ async function postDataNewOnly(data: Record<string, string[]>) {
138
138
  adminforth.list.refresh();
139
139
  }
140
140
  adminforth.alert({
141
- message: `Imported ${resp.importedCount || 0} records. ${resp.errors?.length ? `Errors: ${resp.errors.join(', ')}` : ''}`,
141
+ message: `Imported ${resp.importedCount || 0} records. ${resp.errors?.length ? `First error: ${resp.errors[0]}` : ''}`,
142
142
  variant: resp.errors?.length ? 'warning' : 'success'
143
143
  });
144
144
 
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export default class ImportExport extends AdminForthPlugin {
7
7
  authResourceId: string;
8
8
  adminforth: IAdminForth;
9
9
  constructor(options: PluginOptions);
10
+ private isRowValid;
10
11
  modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource): Promise<void>;
11
12
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
12
13
  instanceUniqueRepresentation(pluginOptions: any): string;
package/dist/index.js CHANGED
@@ -7,12 +7,30 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { AdminForthPlugin, suggestIfTypo, AdminForthFilterOperators, Filters, AdminForthDataTypes } from "adminforth";
10
+ import { AdminForthPlugin, suggestIfTypo, AdminForthFilterOperators, Filters, AdminForthDataTypes, rejectApiRawFilters } from "adminforth";
11
+ import pLimit from 'p-limit';
11
12
  export default class ImportExport extends AdminForthPlugin {
12
13
  constructor(options) {
13
14
  super(options, import.meta.url);
14
15
  this.options = options;
15
16
  }
17
+ isRowValid(row) {
18
+ let errors = [];
19
+ for (const col of Object.keys(row)) {
20
+ const resourceCol = this.resourceConfig.columns.find(c => c.name === col);
21
+ if (!resourceCol) {
22
+ errors.push(`Column '${col}' not found in resource configuration.`);
23
+ continue;
24
+ }
25
+ if (resourceCol.backendOnly) {
26
+ errors.push(`Column '${col}' is backend only and cannot be imported.`);
27
+ }
28
+ if (resourceCol.enum && !resourceCol.enum.some(e => e.value === row[col])) {
29
+ errors.push(`Column '${col}' has an enum of [${resourceCol.enum.map(e => e.label).join(', ')}] but got value '${row[col]}'.`);
30
+ }
31
+ }
32
+ return errors;
33
+ }
16
34
  modifyResourceConfig(adminforth, resourceConfig) {
17
35
  const _super = Object.create(null, {
18
36
  modifyResourceConfig: { get: () => super.modifyResourceConfig }
@@ -55,6 +73,10 @@ export default class ImportExport extends AdminForthPlugin {
55
73
  path: `/plugin/${this.pluginInstanceId}/export-csv`,
56
74
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
57
75
  const { filters, sort } = body;
76
+ const rawFilterError = rejectApiRawFilters(body.filters);
77
+ if (rawFilterError) {
78
+ return rawFilterError;
79
+ }
58
80
  const data = yield this.adminforth.connectors[this.resourceConfig.dataSource].getData({
59
81
  resource: this.resourceConfig,
60
82
  limit: 1e6,
@@ -64,7 +86,7 @@ export default class ImportExport extends AdminForthPlugin {
64
86
  getTotals: true,
65
87
  });
66
88
  // prepare data for PapaParse unparse
67
- const columns = this.resourceConfig.columns.filter((col) => !col.virtual);
89
+ const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
68
90
  const columnsToForceQuote = columns.map(col => {
69
91
  return col.type !== AdminForthDataTypes.FLOAT
70
92
  && col.type !== AdminForthDataTypes.INTEGER
@@ -85,10 +107,11 @@ export default class ImportExport extends AdminForthPlugin {
85
107
  server.endpoint({
86
108
  method: 'POST',
87
109
  path: `/plugin/${this.pluginInstanceId}/import-csv`,
88
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
110
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, query, headers, cookies, requestUrl, response }) {
89
111
  const { data } = body;
90
112
  const columns = this.getColumnNames(data);
91
113
  const { errors, resourceColumns } = this.validateColumns(columns);
114
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
92
115
  if (errors.length > 0) {
93
116
  return { ok: false, errors };
94
117
  }
@@ -97,34 +120,58 @@ export default class ImportExport extends AdminForthPlugin {
97
120
  console.log('Prepared rows for import:', rows);
98
121
  let importedCount = 0;
99
122
  let updatedCount = 0;
100
- yield Promise.all(rows.map((row) => __awaiter(this, void 0, void 0, function* () {
123
+ const limit = pLimit(100);
124
+ yield Promise.all(rows.map((row) => limit(() => __awaiter(this, void 0, void 0, function* () {
101
125
  try {
102
- if (primaryKeyColumn && row[primaryKeyColumn.name]) {
126
+ const rowErrors = yield this.isRowValid(row);
127
+ if (rowErrors.length > 0) {
128
+ errors.push(...rowErrors);
129
+ return;
130
+ }
131
+ const recordId = primaryKeyColumn ? row[primaryKeyColumn.name] : undefined;
132
+ if (primaryKeyColumn && recordId) {
103
133
  const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
104
- .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
134
+ .list([Filters.EQ(primaryKeyColumn.name, recordId)]);
105
135
  if (existingRecord.length > 0) {
106
- yield this.adminforth.resource(this.resourceConfig.resourceId)
107
- .update(row[primaryKeyColumn.name], row);
136
+ const connector = this.adminforth.connectors[resource.dataSource];
137
+ const oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
138
+ if (!oldRecord) {
139
+ const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
140
+ return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
141
+ }
142
+ const { error } = yield this.adminforth.updateResourceRecord({
143
+ resource, updates: row, adminUser, oldRecord, recordId, response,
144
+ extra: { body, query, headers, cookies, requestUrl, response }
145
+ });
146
+ if (error) {
147
+ return { error };
148
+ }
108
149
  updatedCount++;
109
150
  return;
110
151
  }
111
152
  }
112
- yield this.adminforth.resource(this.resourceConfig.resourceId).create(row);
153
+ yield this.adminforth.createResourceRecord({
154
+ resource: resource,
155
+ record: row,
156
+ adminUser: adminUser,
157
+ extra: { body, query, headers, cookies, requestUrl, response }
158
+ });
113
159
  importedCount++;
114
160
  }
115
161
  catch (e) {
116
162
  errors.push(e.message);
117
163
  }
118
- })));
164
+ }))));
119
165
  return { ok: true, importedCount, updatedCount, errors };
120
166
  })
121
167
  });
122
168
  server.endpoint({
123
169
  method: 'POST',
124
170
  path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
125
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
171
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, query, headers, cookies, requestUrl, response }) {
126
172
  const { data } = body;
127
173
  const columns = this.getColumnNames(data);
174
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
128
175
  const { errors, resourceColumns } = this.validateColumns(columns);
129
176
  if (errors.length > 0) {
130
177
  return { ok: false, errors };
@@ -132,8 +179,14 @@ export default class ImportExport extends AdminForthPlugin {
132
179
  const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
133
180
  const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
134
181
  let importedCount = 0;
135
- yield Promise.all(rows.map((row) => __awaiter(this, void 0, void 0, function* () {
182
+ const limit = pLimit(100);
183
+ yield Promise.all(rows.map((row) => limit(() => __awaiter(this, void 0, void 0, function* () {
136
184
  try {
185
+ const rowErrors = yield this.isRowValid(row);
186
+ if (rowErrors.length > 0) {
187
+ errors.push(...rowErrors);
188
+ return;
189
+ }
137
190
  if (primaryKeyColumn && row[primaryKeyColumn.name]) {
138
191
  const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
139
192
  .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
@@ -141,13 +194,18 @@ export default class ImportExport extends AdminForthPlugin {
141
194
  return;
142
195
  }
143
196
  }
144
- yield this.adminforth.resource(this.resourceConfig.resourceId).create(row);
197
+ yield this.adminforth.createResourceRecord({
198
+ resource: resource,
199
+ record: row,
200
+ adminUser: adminUser,
201
+ extra: { body, query, headers, cookies, requestUrl, response }
202
+ });
145
203
  importedCount++;
146
204
  }
147
205
  catch (e) {
148
206
  errors.push(e.message);
149
207
  }
150
- })));
208
+ }))));
151
209
  return { ok: true, importedCount, errors };
152
210
  })
153
211
  });
package/index.ts CHANGED
@@ -1,18 +1,38 @@
1
- import { AdminForthPlugin, suggestIfTypo, AdminForthFilterOperators, Filters, AdminForthDataTypes } from "adminforth";
1
+ import { AdminForthPlugin, suggestIfTypo, AdminForthFilterOperators, Filters, AdminForthDataTypes, rejectApiRawFilters } from "adminforth";
2
2
  import type { IAdminForth, IHttpServer, AdminForthResourceColumn, AdminForthComponentDeclaration, AdminForthResource } from "adminforth";
3
3
  import type { PluginOptions } from './types.js';
4
+ import pLimit from 'p-limit';
4
5
 
5
6
  export default class ImportExport extends AdminForthPlugin {
6
7
  options: PluginOptions;
7
8
  emailField: AdminForthResourceColumn;
8
9
  authResourceId: string;
9
10
  adminforth: IAdminForth;
11
+
10
12
 
11
13
  constructor(options: PluginOptions) {
12
14
  super(options, import.meta.url);
13
15
  this.options = options;
14
16
  }
15
17
 
18
+ private isRowValid(row: Record<string, unknown>): string[] {
19
+ let errors = [];
20
+ for (const col of Object.keys(row)) {
21
+ const resourceCol = this.resourceConfig.columns.find(c => c.name === col);
22
+ if (!resourceCol) {
23
+ errors.push(`Column '${col}' not found in resource configuration.`);
24
+ continue;
25
+ }
26
+ if (resourceCol.backendOnly) {
27
+ errors.push(`Column '${col}' is backend only and cannot be imported.`);
28
+ }
29
+ if (resourceCol.enum && !resourceCol.enum.some(e => e.value === row[col])) {
30
+ errors.push(`Column '${col}' has an enum of [${resourceCol.enum.map(e => e.label).join(', ')}] but got value '${row[col]}'.`);
31
+ }
32
+ }
33
+ return errors;
34
+ }
35
+
16
36
  async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
17
37
  super.modifyResourceConfig(adminforth, resourceConfig);
18
38
  if (!resourceConfig.options.pageInjections) {
@@ -55,7 +75,10 @@ export default class ImportExport extends AdminForthPlugin {
55
75
  path: `/plugin/${this.pluginInstanceId}/export-csv`,
56
76
  handler: async ({ body }) => {
57
77
  const { filters, sort } = body;
58
-
78
+ const rawFilterError = rejectApiRawFilters(body.filters);
79
+ if (rawFilterError) {
80
+ return rawFilterError;
81
+ }
59
82
  const data = await this.adminforth.connectors[this.resourceConfig.dataSource].getData({
60
83
  resource: this.resourceConfig,
61
84
  limit: 1e6,
@@ -66,7 +89,7 @@ export default class ImportExport extends AdminForthPlugin {
66
89
  });
67
90
 
68
91
  // prepare data for PapaParse unparse
69
- const columns = this.resourceConfig.columns.filter((col) => !col.virtual);
92
+ const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
70
93
 
71
94
  const columnsToForceQuote = columns.map(col => {
72
95
  return col.type !== AdminForthDataTypes.FLOAT
@@ -92,10 +115,12 @@ export default class ImportExport extends AdminForthPlugin {
92
115
  server.endpoint({
93
116
  method: 'POST',
94
117
  path: `/plugin/${this.pluginInstanceId}/import-csv`,
95
- handler: async ({ body }) => {
118
+ handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
96
119
  const { data } = body;
97
120
  const columns = this.getColumnNames(data);
98
121
  const { errors, resourceColumns } = this.validateColumns(columns);
122
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
123
+
99
124
  if (errors.length > 0) {
100
125
  return { ok: false, errors };
101
126
  }
@@ -106,26 +131,49 @@ export default class ImportExport extends AdminForthPlugin {
106
131
 
107
132
  let importedCount = 0;
108
133
  let updatedCount = 0;
134
+ const limit = pLimit(100);
109
135
 
110
- await Promise.all(rows.map(async (row) => {
136
+ await Promise.all(rows.map((row) => limit(async () => {
111
137
  try {
112
- if (primaryKeyColumn && row[primaryKeyColumn.name]) {
138
+ const rowErrors = await this.isRowValid(row);
139
+ if (rowErrors.length > 0) {
140
+ errors.push(...rowErrors);
141
+ return;
142
+ }
143
+ const recordId = primaryKeyColumn ? row[primaryKeyColumn.name] as string : undefined;
144
+ if (primaryKeyColumn && recordId) {
113
145
  const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
114
- .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
146
+ .list([Filters.EQ(primaryKeyColumn.name, recordId)]);
115
147
 
116
148
  if (existingRecord.length > 0) {
117
- await this.adminforth.resource(this.resourceConfig.resourceId)
118
- .update(row[primaryKeyColumn.name], row);
149
+ const connector = this.adminforth.connectors[resource.dataSource];
150
+ const oldRecord = await connector.getRecordByPrimaryKey(resource, recordId)
151
+ if (!oldRecord) {
152
+ const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
153
+ return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
154
+ }
155
+ const { error } = await this.adminforth.updateResourceRecord({
156
+ resource, updates: row, adminUser, oldRecord, recordId, response,
157
+ extra: { body, query, headers, cookies, requestUrl, response }
158
+ });
159
+ if (error) {
160
+ return { error };
161
+ }
119
162
  updatedCount++;
120
163
  return;
121
164
  }
122
165
  }
123
- await this.adminforth.resource(this.resourceConfig.resourceId).create(row);
166
+ await this.adminforth.createResourceRecord({
167
+ resource: resource,
168
+ record: row,
169
+ adminUser: adminUser,
170
+ extra: { body, query, headers, cookies, requestUrl, response }
171
+ });
124
172
  importedCount++;
125
173
  } catch (e) {
126
174
  errors.push(e.message);
127
175
  }
128
- }));
176
+ })));
129
177
 
130
178
  return { ok: true, importedCount, updatedCount, errors };
131
179
  }
@@ -134,9 +182,10 @@ export default class ImportExport extends AdminForthPlugin {
134
182
  server.endpoint({
135
183
  method: 'POST',
136
184
  path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
137
- handler: async ({ body }) => {
185
+ handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
138
186
  const { data } = body;
139
187
  const columns = this.getColumnNames(data);
188
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
140
189
  const { errors, resourceColumns } = this.validateColumns(columns);
141
190
  if (errors.length > 0) {
142
191
  return { ok: false, errors };
@@ -146,9 +195,15 @@ export default class ImportExport extends AdminForthPlugin {
146
195
  const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
147
196
 
148
197
  let importedCount = 0;
198
+ const limit = pLimit(100);
149
199
 
150
- await Promise.all(rows.map(async (row) => {
200
+ await Promise.all(rows.map((row) => limit(async () => {
151
201
  try {
202
+ const rowErrors = await this.isRowValid(row);
203
+ if (rowErrors.length > 0) {
204
+ errors.push(...rowErrors);
205
+ return;
206
+ }
152
207
  if (primaryKeyColumn && row[primaryKeyColumn.name]) {
153
208
  const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
154
209
  .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
@@ -157,12 +212,17 @@ export default class ImportExport extends AdminForthPlugin {
157
212
  return;
158
213
  }
159
214
  }
160
- await this.adminforth.resource(this.resourceConfig.resourceId).create(row);
215
+ await this.adminforth.createResourceRecord({
216
+ resource: resource,
217
+ record: row,
218
+ adminUser: adminUser,
219
+ extra: { body, query, headers, cookies, requestUrl, response }
220
+ });
161
221
  importedCount++;
162
222
  } catch (e) {
163
223
  errors.push(e.message);
164
224
  }
165
- }));
225
+ })));
166
226
 
167
227
  return { ok: true, importedCount, errors };
168
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/import-export",
3
- "version": "1.4.25",
3
+ "version": "1.4.27",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.10.7",
32
- "adminforth": "^3.1.0",
32
+ "adminforth": "^3.4.0",
33
33
  "semantic-release": "^24.2.1",
34
34
  "semantic-release-slack-bot": "^4.0.2",
35
35
  "typescript": "^5.7.3"
@@ -58,5 +58,8 @@
58
58
  "prerelease": true
59
59
  }
60
60
  ]
61
+ },
62
+ "dependencies": {
63
+ "p-limit": "^7.3.0"
61
64
  }
62
65
  }
@@ -0,0 +1,4 @@
1
+ allowBuilds:
2
+ adminforth: true
3
+ minimumReleaseAgeExclude:
4
+ - adminforth