@adminforth/import-export 1.4.24 → 1.4.26

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
14
+ sent 17,877 bytes received 134 bytes 36,022.00 bytes/sec
15
15
  total size is 17,395 speedup is 0.97
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 }
@@ -53,9 +71,12 @@ export default class ImportExport extends AdminForthPlugin {
53
71
  server.endpoint({
54
72
  method: 'POST',
55
73
  path: `/plugin/${this.pluginInstanceId}/export-csv`,
56
- noAuth: true,
57
74
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
58
75
  const { filters, sort } = body;
76
+ const rawFilterError = rejectApiRawFilters(body.filters);
77
+ if (rawFilterError) {
78
+ return rawFilterError;
79
+ }
59
80
  const data = yield this.adminforth.connectors[this.resourceConfig.dataSource].getData({
60
81
  resource: this.resourceConfig,
61
82
  limit: 1e6,
@@ -65,7 +86,7 @@ export default class ImportExport extends AdminForthPlugin {
65
86
  getTotals: true,
66
87
  });
67
88
  // prepare data for PapaParse unparse
68
- const columns = this.resourceConfig.columns.filter((col) => !col.virtual);
89
+ const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
69
90
  const columnsToForceQuote = columns.map(col => {
70
91
  return col.type !== AdminForthDataTypes.FLOAT
71
92
  && col.type !== AdminForthDataTypes.INTEGER
@@ -86,11 +107,11 @@ export default class ImportExport extends AdminForthPlugin {
86
107
  server.endpoint({
87
108
  method: 'POST',
88
109
  path: `/plugin/${this.pluginInstanceId}/import-csv`,
89
- noAuth: true,
90
- 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 }) {
91
111
  const { data } = body;
92
112
  const columns = this.getColumnNames(data);
93
113
  const { errors, resourceColumns } = this.validateColumns(columns);
114
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
94
115
  if (errors.length > 0) {
95
116
  return { ok: false, errors };
96
117
  }
@@ -99,35 +120,58 @@ export default class ImportExport extends AdminForthPlugin {
99
120
  console.log('Prepared rows for import:', rows);
100
121
  let importedCount = 0;
101
122
  let updatedCount = 0;
102
- 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* () {
103
125
  try {
104
- 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) {
105
133
  const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
106
- .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
134
+ .list([Filters.EQ(primaryKeyColumn.name, recordId)]);
107
135
  if (existingRecord.length > 0) {
108
- yield this.adminforth.resource(this.resourceConfig.resourceId)
109
- .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
+ }
110
149
  updatedCount++;
111
150
  return;
112
151
  }
113
152
  }
114
- 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
+ });
115
159
  importedCount++;
116
160
  }
117
161
  catch (e) {
118
162
  errors.push(e.message);
119
163
  }
120
- })));
164
+ }))));
121
165
  return { ok: true, importedCount, updatedCount, errors };
122
166
  })
123
167
  });
124
168
  server.endpoint({
125
169
  method: 'POST',
126
170
  path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
127
- noAuth: true,
128
- 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 }) {
129
172
  const { data } = body;
130
173
  const columns = this.getColumnNames(data);
174
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
131
175
  const { errors, resourceColumns } = this.validateColumns(columns);
132
176
  if (errors.length > 0) {
133
177
  return { ok: false, errors };
@@ -135,8 +179,14 @@ export default class ImportExport extends AdminForthPlugin {
135
179
  const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
136
180
  const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
137
181
  let importedCount = 0;
138
- 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* () {
139
184
  try {
185
+ const rowErrors = yield this.isRowValid(row);
186
+ if (rowErrors.length > 0) {
187
+ errors.push(...rowErrors);
188
+ return;
189
+ }
140
190
  if (primaryKeyColumn && row[primaryKeyColumn.name]) {
141
191
  const existingRecord = yield this.adminforth.resource(this.resourceConfig.resourceId)
142
192
  .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
@@ -144,20 +194,24 @@ export default class ImportExport extends AdminForthPlugin {
144
194
  return;
145
195
  }
146
196
  }
147
- 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
+ });
148
203
  importedCount++;
149
204
  }
150
205
  catch (e) {
151
206
  errors.push(e.message);
152
207
  }
153
- })));
208
+ }))));
154
209
  return { ok: true, importedCount, errors };
155
210
  })
156
211
  });
157
212
  server.endpoint({
158
213
  method: 'POST',
159
214
  path: `/plugin/${this.pluginInstanceId}/check-records`,
160
- noAuth: true,
161
215
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
162
216
  const { data } = body;
163
217
  const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
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) {
@@ -53,10 +73,12 @@ export default class ImportExport extends AdminForthPlugin {
53
73
  server.endpoint({
54
74
  method: 'POST',
55
75
  path: `/plugin/${this.pluginInstanceId}/export-csv`,
56
- noAuth: true,
57
76
  handler: async ({ body }) => {
58
77
  const { filters, sort } = body;
59
-
78
+ const rawFilterError = rejectApiRawFilters(body.filters);
79
+ if (rawFilterError) {
80
+ return rawFilterError;
81
+ }
60
82
  const data = await this.adminforth.connectors[this.resourceConfig.dataSource].getData({
61
83
  resource: this.resourceConfig,
62
84
  limit: 1e6,
@@ -67,7 +89,7 @@ export default class ImportExport extends AdminForthPlugin {
67
89
  });
68
90
 
69
91
  // prepare data for PapaParse unparse
70
- const columns = this.resourceConfig.columns.filter((col) => !col.virtual);
92
+ const columns = this.resourceConfig.columns.filter((col) => !col.virtual && !col.backendOnly);
71
93
 
72
94
  const columnsToForceQuote = columns.map(col => {
73
95
  return col.type !== AdminForthDataTypes.FLOAT
@@ -93,11 +115,12 @@ export default class ImportExport extends AdminForthPlugin {
93
115
  server.endpoint({
94
116
  method: 'POST',
95
117
  path: `/plugin/${this.pluginInstanceId}/import-csv`,
96
- noAuth: true,
97
- handler: async ({ body }) => {
118
+ handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
98
119
  const { data } = body;
99
120
  const columns = this.getColumnNames(data);
100
121
  const { errors, resourceColumns } = this.validateColumns(columns);
122
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
123
+
101
124
  if (errors.length > 0) {
102
125
  return { ok: false, errors };
103
126
  }
@@ -108,26 +131,49 @@ export default class ImportExport extends AdminForthPlugin {
108
131
 
109
132
  let importedCount = 0;
110
133
  let updatedCount = 0;
134
+ const limit = pLimit(100);
111
135
 
112
- await Promise.all(rows.map(async (row) => {
136
+ await Promise.all(rows.map((row) => limit(async () => {
113
137
  try {
114
- 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) {
115
145
  const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
116
- .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
146
+ .list([Filters.EQ(primaryKeyColumn.name, recordId)]);
117
147
 
118
148
  if (existingRecord.length > 0) {
119
- await this.adminforth.resource(this.resourceConfig.resourceId)
120
- .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
+ }
121
162
  updatedCount++;
122
163
  return;
123
164
  }
124
165
  }
125
- 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
+ });
126
172
  importedCount++;
127
173
  } catch (e) {
128
174
  errors.push(e.message);
129
175
  }
130
- }));
176
+ })));
131
177
 
132
178
  return { ok: true, importedCount, updatedCount, errors };
133
179
  }
@@ -136,10 +182,10 @@ export default class ImportExport extends AdminForthPlugin {
136
182
  server.endpoint({
137
183
  method: 'POST',
138
184
  path: `/plugin/${this.pluginInstanceId}/import-csv-new-only`,
139
- noAuth: true,
140
- handler: async ({ body }) => {
185
+ handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
141
186
  const { data } = body;
142
187
  const columns = this.getColumnNames(data);
188
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === this.resourceConfig.resourceId);
143
189
  const { errors, resourceColumns } = this.validateColumns(columns);
144
190
  if (errors.length > 0) {
145
191
  return { ok: false, errors };
@@ -149,9 +195,15 @@ export default class ImportExport extends AdminForthPlugin {
149
195
  const rows = this.buildRowsFromData(data, columns, resourceColumns, { coerceTypes: true });
150
196
 
151
197
  let importedCount = 0;
198
+ const limit = pLimit(100);
152
199
 
153
- await Promise.all(rows.map(async (row) => {
200
+ await Promise.all(rows.map((row) => limit(async () => {
154
201
  try {
202
+ const rowErrors = await this.isRowValid(row);
203
+ if (rowErrors.length > 0) {
204
+ errors.push(...rowErrors);
205
+ return;
206
+ }
155
207
  if (primaryKeyColumn && row[primaryKeyColumn.name]) {
156
208
  const existingRecord = await this.adminforth.resource(this.resourceConfig.resourceId)
157
209
  .list([Filters.EQ(primaryKeyColumn.name, row[primaryKeyColumn.name])]);
@@ -160,12 +212,17 @@ export default class ImportExport extends AdminForthPlugin {
160
212
  return;
161
213
  }
162
214
  }
163
- 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
+ });
164
221
  importedCount++;
165
222
  } catch (e) {
166
223
  errors.push(e.message);
167
224
  }
168
- }));
225
+ })));
169
226
 
170
227
  return { ok: true, importedCount, errors };
171
228
  }
@@ -174,7 +231,6 @@ export default class ImportExport extends AdminForthPlugin {
174
231
  server.endpoint({
175
232
  method: 'POST',
176
233
  path: `/plugin/${this.pluginInstanceId}/check-records`,
177
- noAuth: true,
178
234
  handler: async ({ body }) => {
179
235
  const { data } = body as { data: Record<string, unknown[]> };
180
236
  const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/import-export",
3
- "version": "1.4.24",
3
+ "version": "1.4.26",
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