@adminforth/upload 2.8.7 → 2.10.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/build.log CHANGED
@@ -11,5 +11,5 @@ custom/preview.vue
11
11
  custom/tsconfig.json
12
12
  custom/uploader.vue
13
13
 
14
- sent 51,953 bytes received 134 bytes 104,174.00 bytes/sec
15
- total size is 51,471 speedup is 0.99
14
+ sent 53,885 bytes received 134 bytes 108,038.00 bytes/sec
15
+ total size is 53,396 speedup is 0.99
@@ -181,6 +181,7 @@ import { callAdminForthApi } from '@/utils';
181
181
  import { useI18n } from 'vue-i18n';
182
182
  import adminforth from '@/adminforth';
183
183
  import { ProgressBar } from '@/afcl';
184
+ import * as Handlebars from 'handlebars';
184
185
 
185
186
  const { t: $t } = useI18n();
186
187
 
@@ -214,28 +215,9 @@ onMounted(async () => {
214
215
  }
215
216
  // iterate over all variables in template and replace them with their values from props.record[field].
216
217
  // if field is not present in props.record[field] then replace it with empty string and drop warning
217
- const regex = /{{(.*?)}}/g;
218
- const matches = template.match(regex);
219
- if (matches) {
220
- matches.forEach((match) => {
221
- const field = match.replace(/{{|}}/g, '').trim();
222
- if (field in context) {
223
- return;
224
- } else if (field in props.record) {
225
- context[field] = minifyField(props.record[field]);
226
- } else {
227
- adminforth.alert({
228
- message: $t('Field {{field}} defined in template but not found in record', { field }),
229
- variant: 'warning',
230
- timeout: 15,
231
- });
232
- }
233
- });
234
- }
235
-
236
- prompt.value = template.replace(regex, (_, field) => {
237
- return context[field.trim()] || '';
238
- });
218
+ const tpl = Handlebars.compile(template);
219
+ const compiledTemplate = tpl(props.record);
220
+ prompt.value = compiledTemplate;
239
221
 
240
222
  const recordId = props.record[props.meta.recorPkFieldName];
241
223
  if (!recordId) return;
@@ -10,6 +10,7 @@
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@iconify-prerendered/vue-mdi": "^0.25.1718880438",
13
+ "handlebars": "^4.7.8",
13
14
  "medium-zoom": "^1.1.0"
14
15
  }
15
16
  },
@@ -201,6 +202,27 @@
201
202
  "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
202
203
  "peer": true
203
204
  },
205
+ "node_modules/handlebars": {
206
+ "version": "4.7.8",
207
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
208
+ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
209
+ "license": "MIT",
210
+ "dependencies": {
211
+ "minimist": "^1.2.5",
212
+ "neo-async": "^2.6.2",
213
+ "source-map": "^0.6.1",
214
+ "wordwrap": "^1.0.0"
215
+ },
216
+ "bin": {
217
+ "handlebars": "bin/handlebars"
218
+ },
219
+ "engines": {
220
+ "node": ">=0.4.7"
221
+ },
222
+ "optionalDependencies": {
223
+ "uglify-js": "^3.1.4"
224
+ }
225
+ },
204
226
  "node_modules/magic-string": {
205
227
  "version": "0.30.11",
206
228
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
@@ -215,6 +237,15 @@
215
237
  "resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz",
216
238
  "integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="
217
239
  },
240
+ "node_modules/minimist": {
241
+ "version": "1.2.8",
242
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
243
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
244
+ "license": "MIT",
245
+ "funding": {
246
+ "url": "https://github.com/sponsors/ljharb"
247
+ }
248
+ },
218
249
  "node_modules/nanoid": {
219
250
  "version": "3.3.7",
220
251
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -233,6 +264,12 @@
233
264
  "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
234
265
  }
235
266
  },
267
+ "node_modules/neo-async": {
268
+ "version": "2.6.2",
269
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
270
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
271
+ "license": "MIT"
272
+ },
236
273
  "node_modules/picocolors": {
237
274
  "version": "1.1.0",
238
275
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
@@ -267,6 +304,15 @@
267
304
  "node": "^10 || ^12 || >=14"
268
305
  }
269
306
  },
307
+ "node_modules/source-map": {
308
+ "version": "0.6.1",
309
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
310
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
311
+ "license": "BSD-3-Clause",
312
+ "engines": {
313
+ "node": ">=0.10.0"
314
+ }
315
+ },
270
316
  "node_modules/source-map-js": {
271
317
  "version": "1.2.1",
272
318
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -285,6 +331,19 @@
285
331
  "node": ">=4"
286
332
  }
287
333
  },
334
+ "node_modules/uglify-js": {
335
+ "version": "3.19.3",
336
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
337
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
338
+ "license": "BSD-2-Clause",
339
+ "optional": true,
340
+ "bin": {
341
+ "uglifyjs": "bin/uglifyjs"
342
+ },
343
+ "engines": {
344
+ "node": ">=0.8.0"
345
+ }
346
+ },
288
347
  "node_modules/vue": {
289
348
  "version": "3.5.10",
290
349
  "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.10.tgz",
@@ -305,6 +364,12 @@
305
364
  "optional": true
306
365
  }
307
366
  }
367
+ },
368
+ "node_modules/wordwrap": {
369
+ "version": "1.0.0",
370
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
371
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
372
+ "license": "MIT"
308
373
  }
309
374
  }
310
375
  }
@@ -11,6 +11,7 @@
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
13
  "@iconify-prerendered/vue-mdi": "^0.25.1718880438",
14
+ "handlebars": "^4.7.8",
14
15
  "medium-zoom": "^1.1.0"
15
16
  }
16
17
  }
@@ -181,6 +181,7 @@ import { callAdminForthApi } from '@/utils';
181
181
  import { useI18n } from 'vue-i18n';
182
182
  import adminforth from '@/adminforth';
183
183
  import { ProgressBar } from '@/afcl';
184
+ import * as Handlebars from 'handlebars';
184
185
 
185
186
  const { t: $t } = useI18n();
186
187
 
@@ -214,28 +215,9 @@ onMounted(async () => {
214
215
  }
215
216
  // iterate over all variables in template and replace them with their values from props.record[field].
216
217
  // if field is not present in props.record[field] then replace it with empty string and drop warning
217
- const regex = /{{(.*?)}}/g;
218
- const matches = template.match(regex);
219
- if (matches) {
220
- matches.forEach((match) => {
221
- const field = match.replace(/{{|}}/g, '').trim();
222
- if (field in context) {
223
- return;
224
- } else if (field in props.record) {
225
- context[field] = minifyField(props.record[field]);
226
- } else {
227
- adminforth.alert({
228
- message: $t('Field {{field}} defined in template but not found in record', { field }),
229
- variant: 'warning',
230
- timeout: 15,
231
- });
232
- }
233
- });
234
- }
235
-
236
- prompt.value = template.replace(regex, (_, field) => {
237
- return context[field.trim()] || '';
238
- });
218
+ const tpl = Handlebars.compile(template);
219
+ const compiledTemplate = tpl(props.record);
220
+ prompt.value = compiledTemplate;
239
221
 
240
222
  const recordId = props.record[props.meta.recorPkFieldName];
241
223
  if (!recordId) return;
@@ -10,6 +10,7 @@
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@iconify-prerendered/vue-mdi": "^0.25.1718880438",
13
+ "handlebars": "^4.7.8",
13
14
  "medium-zoom": "^1.1.0"
14
15
  }
15
16
  },
@@ -201,6 +202,27 @@
201
202
  "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
202
203
  "peer": true
203
204
  },
205
+ "node_modules/handlebars": {
206
+ "version": "4.7.8",
207
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
208
+ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
209
+ "license": "MIT",
210
+ "dependencies": {
211
+ "minimist": "^1.2.5",
212
+ "neo-async": "^2.6.2",
213
+ "source-map": "^0.6.1",
214
+ "wordwrap": "^1.0.0"
215
+ },
216
+ "bin": {
217
+ "handlebars": "bin/handlebars"
218
+ },
219
+ "engines": {
220
+ "node": ">=0.4.7"
221
+ },
222
+ "optionalDependencies": {
223
+ "uglify-js": "^3.1.4"
224
+ }
225
+ },
204
226
  "node_modules/magic-string": {
205
227
  "version": "0.30.11",
206
228
  "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
@@ -215,6 +237,15 @@
215
237
  "resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz",
216
238
  "integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ=="
217
239
  },
240
+ "node_modules/minimist": {
241
+ "version": "1.2.8",
242
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
243
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
244
+ "license": "MIT",
245
+ "funding": {
246
+ "url": "https://github.com/sponsors/ljharb"
247
+ }
248
+ },
218
249
  "node_modules/nanoid": {
219
250
  "version": "3.3.7",
220
251
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -233,6 +264,12 @@
233
264
  "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
234
265
  }
235
266
  },
267
+ "node_modules/neo-async": {
268
+ "version": "2.6.2",
269
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
270
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
271
+ "license": "MIT"
272
+ },
236
273
  "node_modules/picocolors": {
237
274
  "version": "1.1.0",
238
275
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
@@ -267,6 +304,15 @@
267
304
  "node": "^10 || ^12 || >=14"
268
305
  }
269
306
  },
307
+ "node_modules/source-map": {
308
+ "version": "0.6.1",
309
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
310
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
311
+ "license": "BSD-3-Clause",
312
+ "engines": {
313
+ "node": ">=0.10.0"
314
+ }
315
+ },
270
316
  "node_modules/source-map-js": {
271
317
  "version": "1.2.1",
272
318
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -285,6 +331,19 @@
285
331
  "node": ">=4"
286
332
  }
287
333
  },
334
+ "node_modules/uglify-js": {
335
+ "version": "3.19.3",
336
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
337
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
338
+ "license": "BSD-2-Clause",
339
+ "optional": true,
340
+ "bin": {
341
+ "uglifyjs": "bin/uglifyjs"
342
+ },
343
+ "engines": {
344
+ "node": ">=0.8.0"
345
+ }
346
+ },
288
347
  "node_modules/vue": {
289
348
  "version": "3.5.10",
290
349
  "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.10.tgz",
@@ -305,6 +364,12 @@
305
364
  "optional": true
306
365
  }
307
366
  }
367
+ },
368
+ "node_modules/wordwrap": {
369
+ "version": "1.0.0",
370
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
371
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
372
+ "license": "MIT"
308
373
  }
309
374
  }
310
375
  }
@@ -11,6 +11,7 @@
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
13
  "@iconify-prerendered/vue-mdi": "^0.25.1718880438",
14
+ "handlebars": "^4.7.8",
14
15
  "medium-zoom": "^1.1.0"
15
16
  }
16
17
  }
package/dist/index.js CHANGED
@@ -7,9 +7,8 @@ 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, Filters, suggestIfTypo } from "adminforth";
10
+ import { AdminForthPlugin, Filters, suggestIfTypo, RateLimiter } from "adminforth";
11
11
  import { Readable } from "stream";
12
- import { RateLimiter } from "adminforth";
13
12
  import { randomUUID } from "crypto";
14
13
  import { interpretResource } from 'adminforth';
15
14
  import { ActionCheckSource } from 'adminforth';
@@ -186,7 +185,7 @@ export default class UploadPlugin extends AdminForthPlugin {
186
185
  modifyResourceConfig: { get: () => super.modifyResourceConfig }
187
186
  });
188
187
  return __awaiter(this, void 0, void 0, function* () {
189
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
188
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
190
189
  _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
191
190
  this.resourceConfig = resourceConfig;
192
191
  // after column to store the path of the uploaded file, add new VirtualColumn,
@@ -197,15 +196,6 @@ export default class UploadPlugin extends AdminForthPlugin {
197
196
  if (pathColumnIndex === -1) {
198
197
  throw new Error(`Column with name "${pathColumnName}" not found in resource "${resourceConfig.label}"`);
199
198
  }
200
- if ((_a = this.options.generation) === null || _a === void 0 ? void 0 : _a.fieldsForContext) {
201
- (_b = this.options.generation) === null || _b === void 0 ? void 0 : _b.fieldsForContext.forEach((field) => {
202
- if (!resourceConfig.columns.find((column) => column.name === field)) {
203
- const similar = suggestIfTypo(resourceConfig.columns.map((column) => column.name), field);
204
- throw new Error(`Field "${field}" specified in fieldsForContext not found in
205
- resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
206
- }
207
- });
208
- }
209
199
  const pluginFrontendOptions = {
210
200
  allowedExtensions: this.options.allowedFileExtensions,
211
201
  maxFileSize: this.options.maxFileSize,
@@ -213,14 +203,14 @@ export default class UploadPlugin extends AdminForthPlugin {
213
203
  resourceLabel: resourceConfig.label,
214
204
  generateImages: this.options.generation ? true : false,
215
205
  pathColumnLabel: resourceConfig.columns[pathColumnIndex].label,
216
- maxWidth: (_c = this.options.preview) === null || _c === void 0 ? void 0 : _c.maxWidth,
217
- maxListWidth: (_d = this.options.preview) === null || _d === void 0 ? void 0 : _d.maxListWidth,
218
- maxShowWidth: (_e = this.options.preview) === null || _e === void 0 ? void 0 : _e.maxShowWidth,
219
- minWidth: (_f = this.options.preview) === null || _f === void 0 ? void 0 : _f.minWidth,
220
- minListWidth: (_g = this.options.preview) === null || _g === void 0 ? void 0 : _g.minListWidth,
221
- minShowWidth: (_h = this.options.preview) === null || _h === void 0 ? void 0 : _h.minShowWidth,
222
- generationPrompt: (_j = this.options.generation) === null || _j === void 0 ? void 0 : _j.generationPrompt,
223
- recorPkFieldName: (_k = this.resourceConfig.columns.find((column) => column.primaryKey)) === null || _k === void 0 ? void 0 : _k.name,
206
+ maxWidth: (_a = this.options.preview) === null || _a === void 0 ? void 0 : _a.maxWidth,
207
+ maxListWidth: (_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.maxListWidth,
208
+ maxShowWidth: (_c = this.options.preview) === null || _c === void 0 ? void 0 : _c.maxShowWidth,
209
+ minWidth: (_d = this.options.preview) === null || _d === void 0 ? void 0 : _d.minWidth,
210
+ minListWidth: (_e = this.options.preview) === null || _e === void 0 ? void 0 : _e.minListWidth,
211
+ minShowWidth: (_f = this.options.preview) === null || _f === void 0 ? void 0 : _f.minShowWidth,
212
+ generationPrompt: (_g = this.options.generation) === null || _g === void 0 ? void 0 : _g.generationPrompt,
213
+ recorPkFieldName: (_h = this.resourceConfig.columns.find((column) => column.primaryKey)) === null || _h === void 0 ? void 0 : _h.name,
224
214
  pathColumnName: this.options.pathColumnName,
225
215
  };
226
216
  // define components which will be imported from other components
@@ -230,7 +220,7 @@ export default class UploadPlugin extends AdminForthPlugin {
230
220
  }
231
221
  const pathColumn = resourceConfig.columns[pathColumnIndex];
232
222
  // add preview column to list
233
- if (((_l = this.options.preview) === null || _l === void 0 ? void 0 : _l.usePreviewComponents) !== false) {
223
+ if (((_j = this.options.preview) === null || _j === void 0 ? void 0 : _j.usePreviewComponents) !== false) {
234
224
  resourceConfig.columns[pathColumnIndex].components.list = {
235
225
  file: this.componentPath('preview.vue'),
236
226
  meta: pluginFrontendOptions,
@@ -333,7 +323,28 @@ export default class UploadPlugin extends AdminForthPlugin {
333
323
  });
334
324
  }
335
325
  validateConfigAfterDiscover(adminforth, resourceConfig) {
326
+ var _a;
336
327
  this.adminforth = adminforth;
328
+ if (this.options.generation) {
329
+ const template = (_a = this.options.generation) === null || _a === void 0 ? void 0 : _a.generationPrompt;
330
+ const regex = /{{(.*?)}}/g;
331
+ const matches = template.match(regex);
332
+ if (matches) {
333
+ matches.forEach((match) => {
334
+ const field = match.replace(/{{|}}/g, '').trim();
335
+ if (!resourceConfig.columns.find((column) => column.name === field)) {
336
+ const similar = suggestIfTypo(resourceConfig.columns.map((column) => column.name), field);
337
+ throw new Error(`Field "${field}" specified in generationPrompt not found in resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
338
+ }
339
+ else {
340
+ let column = resourceConfig.columns.find((column) => column.name === field);
341
+ if (column.backendOnly === true) {
342
+ throw new Error(`Field "${field}" specified in generationPrompt is marked as backendOnly in resource "${resourceConfig.label}". Please remove backendOnly or choose another field.`);
343
+ }
344
+ }
345
+ });
346
+ }
347
+ }
337
348
  // called here because modifyResourceConfig can be called in build time where there is no environment and AWS secrets
338
349
  this.setupLifecycleRule();
339
350
  }
@@ -471,4 +482,101 @@ export default class UploadPlugin extends AdminForthPlugin {
471
482
  }),
472
483
  });
473
484
  }
485
+ uploadFromBuffer(_a) {
486
+ return __awaiter(this, arguments, void 0, function* ({ filename, contentType, buffer, adminUser, extra, }) {
487
+ var _b;
488
+ if (!filename || !contentType || !buffer) {
489
+ throw new Error('filename, contentType and buffer are required');
490
+ }
491
+ const lastDotIndex = filename.lastIndexOf('.');
492
+ if (lastDotIndex === -1) {
493
+ throw new Error('filename must contain an extension');
494
+ }
495
+ const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
496
+ const originalFilename = filename.substring(0, lastDotIndex);
497
+ if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
498
+ throw new Error(`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`);
499
+ }
500
+ let nodeBuffer;
501
+ if (Buffer.isBuffer(buffer)) {
502
+ nodeBuffer = buffer;
503
+ }
504
+ else if (buffer instanceof ArrayBuffer) {
505
+ nodeBuffer = Buffer.from(buffer);
506
+ }
507
+ else if (ArrayBuffer.isView(buffer)) {
508
+ nodeBuffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
509
+ }
510
+ else {
511
+ throw new Error('Unsupported buffer type');
512
+ }
513
+ const size = nodeBuffer.byteLength;
514
+ if (this.options.maxFileSize && size > this.options.maxFileSize) {
515
+ throw new Error(`File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`);
516
+ }
517
+ const filePath = this.options.filePath({
518
+ originalFilename,
519
+ originalExtension,
520
+ contentType,
521
+ record: undefined,
522
+ });
523
+ if (filePath.startsWith('/')) {
524
+ throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
525
+ }
526
+ const { uploadUrl, uploadExtraParams } = yield this.options.storageAdapter.getUploadSignedUrl(filePath, contentType, 1800);
527
+ const headers = {
528
+ 'Content-Type': contentType,
529
+ };
530
+ if (uploadExtraParams) {
531
+ Object.entries(uploadExtraParams).forEach(([key, value]) => {
532
+ headers[key] = value;
533
+ });
534
+ }
535
+ const resp = yield fetch(uploadUrl, {
536
+ method: 'PUT',
537
+ headers,
538
+ body: nodeBuffer,
539
+ });
540
+ if (!resp.ok) {
541
+ let bodyText = '';
542
+ try {
543
+ bodyText = yield resp.text();
544
+ }
545
+ catch (e) {
546
+ // ignore
547
+ }
548
+ throw new Error(`Upload failed with status ${resp.status}: ${bodyText}`);
549
+ }
550
+ yield this.options.storageAdapter.markKeyForNotDeletation(filePath);
551
+ if (!this.resourceConfig) {
552
+ throw new Error('resourceConfig is not initialized yet');
553
+ }
554
+ const { error: createError } = yield this.adminforth.createResourceRecord({
555
+ resource: this.resourceConfig,
556
+ record: { [this.options.pathColumnName]: filePath },
557
+ adminUser,
558
+ extra,
559
+ });
560
+ if (createError) {
561
+ try {
562
+ yield this.options.storageAdapter.markKeyForDeletation(filePath);
563
+ }
564
+ catch (e) {
565
+ // best-effort cleanup, ignore error
566
+ }
567
+ throw new Error(`Error creating record after upload: ${createError}`);
568
+ }
569
+ let previewUrl;
570
+ if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
571
+ previewUrl = this.options.preview.previewUrl({ filePath });
572
+ }
573
+ else {
574
+ previewUrl = yield this.options.storageAdapter.getDownloadUrl(filePath, 1800);
575
+ }
576
+ return {
577
+ path: filePath,
578
+ previewUrl,
579
+ };
580
+ });
581
+ }
474
582
  }
package/index.ts CHANGED
@@ -1,8 +1,7 @@
1
1
 
2
2
  import { PluginOptions } from './types.js';
3
- import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo } from "adminforth";
3
+ import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo, RateLimiter, AdminUser, HttpExtra } from "adminforth";
4
4
  import { Readable } from "stream";
5
- import { RateLimiter } from "adminforth";
6
5
  import { randomUUID } from "crypto";
7
6
  import { interpretResource } from 'adminforth';
8
7
  import { ActionCheckSource } from 'adminforth';
@@ -219,16 +218,6 @@ export default class UploadPlugin extends AdminForthPlugin {
219
218
  throw new Error(`Column with name "${pathColumnName}" not found in resource "${resourceConfig.label}"`);
220
219
  }
221
220
 
222
- if (this.options.generation?.fieldsForContext) {
223
- this.options.generation?.fieldsForContext.forEach((field: string) => {
224
- if (!resourceConfig.columns.find((column: any) => column.name === field)) {
225
- const similar = suggestIfTypo(resourceConfig.columns.map((column: any) => column.name), field);
226
- throw new Error(`Field "${field}" specified in fieldsForContext not found in
227
- resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
228
- }
229
- });
230
- }
231
-
232
221
  const pluginFrontendOptions = {
233
222
  allowedExtensions: this.options.allowedFileExtensions,
234
223
  maxFileSize: this.options.maxFileSize,
@@ -386,6 +375,26 @@ export default class UploadPlugin extends AdminForthPlugin {
386
375
 
387
376
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: any) {
388
377
  this.adminforth = adminforth;
378
+
379
+ if (this.options.generation) {
380
+ const template = this.options.generation?.generationPrompt;
381
+ const regex = /{{(.*?)}}/g;
382
+ const matches = template.match(regex);
383
+ if (matches) {
384
+ matches.forEach((match) => {
385
+ const field = match.replace(/{{|}}/g, '').trim();
386
+ if (!resourceConfig.columns.find((column: any) => column.name === field)) {
387
+ const similar = suggestIfTypo(resourceConfig.columns.map((column: any) => column.name), field);
388
+ throw new Error(`Field "${field}" specified in generationPrompt not found in resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
389
+ } else {
390
+ let column = resourceConfig.columns.find((column: any) => column.name === field);
391
+ if (column.backendOnly === true) {
392
+ throw new Error(`Field "${field}" specified in generationPrompt is marked as backendOnly in resource "${resourceConfig.label}". Please remove backendOnly or choose another field.`);
393
+ }
394
+ }
395
+ });
396
+ }
397
+ }
389
398
  // called here because modifyResourceConfig can be called in build time where there is no environment and AWS secrets
390
399
  this.setupLifecycleRule();
391
400
  }
@@ -545,4 +554,129 @@ export default class UploadPlugin extends AdminForthPlugin {
545
554
  }
546
555
 
547
556
 
557
+ async uploadFromBuffer({
558
+ filename,
559
+ contentType,
560
+ buffer,
561
+ adminUser,
562
+ extra,
563
+ }: {
564
+ filename: string;
565
+ contentType: string;
566
+ buffer: Buffer | Uint8Array | ArrayBuffer;
567
+ adminUser: AdminUser;
568
+ extra?: HttpExtra;
569
+ }): Promise<{ path: string; previewUrl: string }> {
570
+ if (!filename || !contentType || !buffer) {
571
+ throw new Error('filename, contentType and buffer are required');
572
+ }
573
+
574
+ const lastDotIndex = filename.lastIndexOf('.');
575
+ if (lastDotIndex === -1) {
576
+ throw new Error('filename must contain an extension');
577
+ }
578
+
579
+ const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
580
+ const originalFilename = filename.substring(0, lastDotIndex);
581
+
582
+ if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
583
+ throw new Error(
584
+ `File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`
585
+ );
586
+ }
587
+
588
+ let nodeBuffer: Buffer;
589
+ if (Buffer.isBuffer(buffer)) {
590
+ nodeBuffer = buffer;
591
+ } else if (buffer instanceof ArrayBuffer) {
592
+ nodeBuffer = Buffer.from(buffer);
593
+ } else if (ArrayBuffer.isView(buffer)) {
594
+ nodeBuffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
595
+ } else {
596
+ throw new Error('Unsupported buffer type');
597
+ }
598
+
599
+ const size = nodeBuffer.byteLength;
600
+ if (this.options.maxFileSize && size > this.options.maxFileSize) {
601
+ throw new Error(
602
+ `File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`
603
+ );
604
+ }
605
+ const filePath: string = this.options.filePath({
606
+ originalFilename,
607
+ originalExtension,
608
+ contentType,
609
+ record: undefined,
610
+ });
611
+
612
+ if (filePath.startsWith('/')) {
613
+ throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
614
+ }
615
+
616
+ const { uploadUrl, uploadExtraParams } = await this.options.storageAdapter.getUploadSignedUrl(
617
+ filePath,
618
+ contentType,
619
+ 1800,
620
+ );
621
+
622
+ const headers: Record<string, string> = {
623
+ 'Content-Type': contentType,
624
+ };
625
+ if (uploadExtraParams) {
626
+ Object.entries(uploadExtraParams).forEach(([key, value]) => {
627
+ headers[key] = value as string;
628
+ });
629
+ }
630
+
631
+ const resp = await fetch(uploadUrl as any, {
632
+ method: 'PUT',
633
+ headers,
634
+ body: nodeBuffer as any,
635
+ });
636
+
637
+ if (!resp.ok) {
638
+ let bodyText = '';
639
+ try {
640
+ bodyText = await resp.text();
641
+ } catch (e) {
642
+ // ignore
643
+ }
644
+ throw new Error(`Upload failed with status ${resp.status}: ${bodyText}`);
645
+ }
646
+
647
+ await this.options.storageAdapter.markKeyForNotDeletation(filePath);
648
+
649
+ if (!this.resourceConfig) {
650
+ throw new Error('resourceConfig is not initialized yet');
651
+ }
652
+
653
+ const { error: createError } = await this.adminforth.createResourceRecord({
654
+ resource: this.resourceConfig,
655
+ record: { [this.options.pathColumnName]: filePath },
656
+ adminUser,
657
+ extra,
658
+ });
659
+
660
+ if (createError) {
661
+ try {
662
+ await this.options.storageAdapter.markKeyForDeletation(filePath);
663
+ } catch (e) {
664
+ // best-effort cleanup, ignore error
665
+ }
666
+ throw new Error(`Error creating record after upload: ${createError}`);
667
+ }
668
+
669
+ let previewUrl: string;
670
+ if (this.options.preview?.previewUrl) {
671
+ previewUrl = this.options.preview.previewUrl({ filePath });
672
+ } else {
673
+ previewUrl = await this.options.storageAdapter.getDownloadUrl(filePath, 1800);
674
+ }
675
+
676
+ return {
677
+ path: filePath,
678
+ previewUrl,
679
+ };
680
+ }
681
+
548
682
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/upload",
3
- "version": "2.8.7",
3
+ "version": "2.10.0",
4
4
  "description": "Plugin for uploading files for adminforth",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/types.ts CHANGED
@@ -102,12 +102,6 @@ export type PluginOptions = {
102
102
  */
103
103
  outputSize?: string,
104
104
 
105
- /**
106
- * Fields for conetext which will be used to generate the image.
107
- * If specified, the plugin will use fields from the record to provide additional context to the AI model.
108
- */
109
- fieldsForContext?: string[],
110
-
111
105
  /**
112
106
  * The number of images to generate
113
107
  * in one request