@adminforth/upload 2.11.0 → 2.12.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 53,878 bytes received 134 bytes 108,024.00 bytes/sec
15
- total size is 53,396 speedup is 0.99
14
+ sent 54,028 bytes received 134 bytes 108,324.00 bytes/sec
15
+ total size is 53,539 speedup is 0.99
@@ -84,7 +84,10 @@ onMounted(async () => {
84
84
  if (Array.isArray(url.value)) return;
85
85
  try {
86
86
  const response = await fetch(url.value, {
87
- method: 'HEAD',
87
+ method: 'GET',
88
+ headers: {
89
+ Range: 'bytes=0-0' // this approach is better then HEAD in terms of CDN/CORS, but same for efficiency as HEAD
90
+ },
88
91
  mode: 'cors',
89
92
  });
90
93
  const ct = response.headers.get('Content-Type');
@@ -84,7 +84,10 @@ onMounted(async () => {
84
84
  if (Array.isArray(url.value)) return;
85
85
  try {
86
86
  const response = await fetch(url.value, {
87
- method: 'HEAD',
87
+ method: 'GET',
88
+ headers: {
89
+ Range: 'bytes=0-0' // this approach is better then HEAD in terms of CDN/CORS, but same for efficiency as HEAD
90
+ },
88
91
  mode: 'cors',
89
92
  });
90
93
  const ct = response.headers.get('Content-Type');
package/dist/index.js CHANGED
@@ -485,7 +485,7 @@ export default class UploadPlugin extends AdminForthPlugin {
485
485
  /*
486
486
  * Uploads a file from a buffer, creates a record in the resource, and returns the file path and preview URL.
487
487
  */
488
- uploadFromBuffer(_a) {
488
+ uploadFromBufferToNewRecord(_a) {
489
489
  return __awaiter(this, arguments, void 0, function* ({ filename, contentType, buffer, adminUser, extra, recordAttributes, }) {
490
490
  var _b;
491
491
  if (!filename || !contentType || !buffer) {
@@ -554,7 +554,7 @@ export default class UploadPlugin extends AdminForthPlugin {
554
554
  if (!this.resourceConfig) {
555
555
  throw new Error('resourceConfig is not initialized yet');
556
556
  }
557
- const { error: createError } = yield this.adminforth.createResourceRecord({
557
+ const { error: createError, createdRecord, newRecordId } = yield this.adminforth.createResourceRecord({
558
558
  resource: this.resourceConfig,
559
559
  record: Object.assign(Object.assign({}, (recordAttributes !== null && recordAttributes !== void 0 ? recordAttributes : {})), { [this.options.pathColumnName]: filePath }),
560
560
  adminUser,
@@ -569,6 +569,134 @@ export default class UploadPlugin extends AdminForthPlugin {
569
569
  }
570
570
  throw new Error(`Error creating record after upload: ${createError}`);
571
571
  }
572
+ const pkColumn = this.resourceConfig.columns.find((column) => column.primaryKey);
573
+ const pkName = pkColumn === null || pkColumn === void 0 ? void 0 : pkColumn.name;
574
+ const newRecordPk = newRecordId !== null && newRecordId !== void 0 ? newRecordId : (pkName && createdRecord ? createdRecord[pkName] : undefined);
575
+ let previewUrl;
576
+ if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
577
+ previewUrl = this.options.preview.previewUrl({ filePath });
578
+ }
579
+ else {
580
+ previewUrl = yield this.options.storageAdapter.getDownloadUrl(filePath, 1800);
581
+ }
582
+ return {
583
+ path: filePath,
584
+ previewUrl,
585
+ newRecordPk,
586
+ };
587
+ });
588
+ }
589
+ /*
590
+ * Uploads a file from a buffer and updates an existing record's path column.
591
+ * If the newly generated storage path would be the same as the current path,
592
+ * throws an error to avoid potential caching issues.
593
+ */
594
+ uploadFromBufferToExistingRecord(_a) {
595
+ return __awaiter(this, arguments, void 0, function* ({ recordId, filename, contentType, buffer, adminUser, extra, }) {
596
+ var _b;
597
+ if (recordId === undefined || recordId === null) {
598
+ throw new Error('recordId is required');
599
+ }
600
+ if (!filename || !contentType || !buffer) {
601
+ throw new Error('filename, contentType and buffer are required');
602
+ }
603
+ if (!this.resourceConfig) {
604
+ throw new Error('resourceConfig is not initialized yet');
605
+ }
606
+ const pkColumn = this.resourceConfig.columns.find((column) => column.primaryKey);
607
+ const pkName = pkColumn === null || pkColumn === void 0 ? void 0 : pkColumn.name;
608
+ if (!pkName) {
609
+ throw new Error('Primary key column not found in resource configuration');
610
+ }
611
+ const existingRecord = yield this.adminforth
612
+ .resource(this.resourceConfig.resourceId)
613
+ .get([Filters.EQ(pkName, recordId)]);
614
+ if (!existingRecord) {
615
+ throw new Error(`Record with id ${recordId} not found`);
616
+ }
617
+ const lastDotIndex = filename.lastIndexOf('.');
618
+ if (lastDotIndex === -1) {
619
+ throw new Error('filename must contain an extension');
620
+ }
621
+ const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
622
+ const originalFilename = filename.substring(0, lastDotIndex);
623
+ if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
624
+ throw new Error(`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`);
625
+ }
626
+ let nodeBuffer;
627
+ if (Buffer.isBuffer(buffer)) {
628
+ nodeBuffer = buffer;
629
+ }
630
+ else if (buffer instanceof ArrayBuffer) {
631
+ nodeBuffer = Buffer.from(buffer);
632
+ }
633
+ else if (ArrayBuffer.isView(buffer)) {
634
+ nodeBuffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
635
+ }
636
+ else {
637
+ throw new Error('Unsupported buffer type');
638
+ }
639
+ const size = nodeBuffer.byteLength;
640
+ if (this.options.maxFileSize && size > this.options.maxFileSize) {
641
+ throw new Error(`File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`);
642
+ }
643
+ const existingValue = existingRecord[this.options.pathColumnName];
644
+ const existingPaths = this.normalizePaths(existingValue);
645
+ const filePath = this.options.filePath({
646
+ originalFilename,
647
+ originalExtension,
648
+ contentType,
649
+ record: existingRecord,
650
+ });
651
+ if (filePath.startsWith('/')) {
652
+ throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
653
+ }
654
+ if (existingPaths.includes(filePath)) {
655
+ throw new Error('New file path cannot be the same as existing path to avoid caching issues');
656
+ }
657
+ const { uploadUrl, uploadExtraParams } = yield this.options.storageAdapter.getUploadSignedUrl(filePath, contentType, 1800);
658
+ const headers = {
659
+ 'Content-Type': contentType,
660
+ };
661
+ if (uploadExtraParams) {
662
+ Object.entries(uploadExtraParams).forEach(([key, value]) => {
663
+ headers[key] = value;
664
+ });
665
+ }
666
+ const resp = yield fetch(uploadUrl, {
667
+ method: 'PUT',
668
+ headers,
669
+ body: nodeBuffer,
670
+ });
671
+ if (!resp.ok) {
672
+ let bodyText = '';
673
+ try {
674
+ bodyText = yield resp.text();
675
+ }
676
+ catch (e) {
677
+ // ignore
678
+ }
679
+ throw new Error(`Upload failed with status ${resp.status}: ${bodyText}`);
680
+ }
681
+ const { error: updateError } = yield this.adminforth.updateResourceRecord({
682
+ resource: this.resourceConfig,
683
+ recordId,
684
+ oldRecord: existingRecord,
685
+ adminUser,
686
+ extra,
687
+ updates: {
688
+ [this.options.pathColumnName]: filePath,
689
+ },
690
+ });
691
+ if (updateError) {
692
+ try {
693
+ yield this.markKeyForDeletion(filePath);
694
+ }
695
+ catch (e) {
696
+ // best-effort cleanup, ignore error
697
+ }
698
+ throw new Error(`Error updating record after upload: ${updateError}`);
699
+ }
572
700
  let previewUrl;
573
701
  if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
574
702
  previewUrl = this.options.preview.previewUrl({ filePath });
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { PluginOptions, UploadFromBufferParams } from './types.js';
2
+ import { PluginOptions, UploadFromBufferParams, UploadFromBufferToExistingRecordParams } from './types.js';
3
3
  import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo, RateLimiter, AdminUser, HttpExtra } from "adminforth";
4
4
  import { Readable } from "stream";
5
5
  import { randomUUID } from "crypto";
@@ -549,22 +549,19 @@ export default class UploadPlugin extends AdminForthPlugin {
549
549
  return { error: 'failed to generate preview URL' };
550
550
  },
551
551
  });
552
-
553
-
554
552
  }
555
553
 
556
-
557
554
  /*
558
555
  * Uploads a file from a buffer, creates a record in the resource, and returns the file path and preview URL.
559
556
  */
560
- async uploadFromBuffer({
557
+ async uploadFromBufferToNewRecord({
561
558
  filename,
562
559
  contentType,
563
560
  buffer,
564
561
  adminUser,
565
562
  extra,
566
563
  recordAttributes,
567
- }: UploadFromBufferParams): Promise<{ path: string; previewUrl: string }> {
564
+ }: UploadFromBufferParams): Promise<{ path: string; previewUrl: string; newRecordPk: any }> {
568
565
  if (!filename || !contentType || !buffer) {
569
566
  throw new Error('filename, contentType and buffer are required');
570
567
  }
@@ -648,7 +645,7 @@ export default class UploadPlugin extends AdminForthPlugin {
648
645
  throw new Error('resourceConfig is not initialized yet');
649
646
  }
650
647
 
651
- const { error: createError } = await this.adminforth.createResourceRecord({
648
+ const { error: createError, createdRecord, newRecordId }: any = await this.adminforth.createResourceRecord({
652
649
  resource: this.resourceConfig,
653
650
  record: { ...(recordAttributes ?? {}), [this.options.pathColumnName]: filePath },
654
651
  adminUser,
@@ -664,6 +661,164 @@ export default class UploadPlugin extends AdminForthPlugin {
664
661
  throw new Error(`Error creating record after upload: ${createError}`);
665
662
  }
666
663
 
664
+ const pkColumn = this.resourceConfig.columns.find((column: any) => column.primaryKey);
665
+ const pkName = pkColumn?.name;
666
+ const newRecordPk = newRecordId ?? (pkName && createdRecord ? createdRecord[pkName] : undefined);
667
+
668
+ let previewUrl: string;
669
+ if (this.options.preview?.previewUrl) {
670
+ previewUrl = this.options.preview.previewUrl({ filePath });
671
+ } else {
672
+ previewUrl = await this.options.storageAdapter.getDownloadUrl(filePath, 1800);
673
+ }
674
+
675
+ return {
676
+ path: filePath,
677
+ previewUrl,
678
+ newRecordPk,
679
+ };
680
+ }
681
+
682
+ /*
683
+ * Uploads a file from a buffer and updates an existing record's path column.
684
+ * If the newly generated storage path would be the same as the current path,
685
+ * throws an error to avoid potential caching issues.
686
+ */
687
+ async uploadFromBufferToExistingRecord({
688
+ recordId,
689
+ filename,
690
+ contentType,
691
+ buffer,
692
+ adminUser,
693
+ extra,
694
+ }: UploadFromBufferToExistingRecordParams): Promise<{ path: string; previewUrl: string }> {
695
+ if (recordId === undefined || recordId === null) {
696
+ throw new Error('recordId is required');
697
+ }
698
+
699
+ if (!filename || !contentType || !buffer) {
700
+ throw new Error('filename, contentType and buffer are required');
701
+ }
702
+
703
+ if (!this.resourceConfig) {
704
+ throw new Error('resourceConfig is not initialized yet');
705
+ }
706
+
707
+ const pkColumn = this.resourceConfig.columns.find((column: any) => column.primaryKey);
708
+ const pkName = pkColumn?.name;
709
+ if (!pkName) {
710
+ throw new Error('Primary key column not found in resource configuration');
711
+ }
712
+
713
+ const existingRecord = await this.adminforth
714
+ .resource(this.resourceConfig.resourceId)
715
+ .get([Filters.EQ(pkName, recordId)]);
716
+
717
+ if (!existingRecord) {
718
+ throw new Error(`Record with id ${recordId} not found`);
719
+ }
720
+
721
+ const lastDotIndex = filename.lastIndexOf('.');
722
+ if (lastDotIndex === -1) {
723
+ throw new Error('filename must contain an extension');
724
+ }
725
+
726
+ const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
727
+ const originalFilename = filename.substring(0, lastDotIndex);
728
+
729
+ if (this.options.allowedFileExtensions && !this.options.allowedFileExtensions.includes(originalExtension)) {
730
+ throw new Error(
731
+ `File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`
732
+ );
733
+ }
734
+
735
+ let nodeBuffer: Buffer;
736
+ if (Buffer.isBuffer(buffer)) {
737
+ nodeBuffer = buffer;
738
+ } else if (buffer instanceof ArrayBuffer) {
739
+ nodeBuffer = Buffer.from(buffer);
740
+ } else if (ArrayBuffer.isView(buffer)) {
741
+ nodeBuffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
742
+ } else {
743
+ throw new Error('Unsupported buffer type');
744
+ }
745
+
746
+ const size = nodeBuffer.byteLength;
747
+ if (this.options.maxFileSize && size > this.options.maxFileSize) {
748
+ throw new Error(
749
+ `File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`
750
+ );
751
+ }
752
+
753
+ const existingValue = existingRecord[this.options.pathColumnName];
754
+ const existingPaths = this.normalizePaths(existingValue);
755
+
756
+ const filePath: string = this.options.filePath({
757
+ originalFilename,
758
+ originalExtension,
759
+ contentType,
760
+ record: existingRecord,
761
+ });
762
+
763
+ if (filePath.startsWith('/')) {
764
+ throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
765
+ }
766
+
767
+ if (existingPaths.includes(filePath)) {
768
+ throw new Error('New file path cannot be the same as existing path to avoid caching issues');
769
+ }
770
+
771
+ const { uploadUrl, uploadExtraParams } = await this.options.storageAdapter.getUploadSignedUrl(
772
+ filePath,
773
+ contentType,
774
+ 1800,
775
+ );
776
+
777
+ const headers: Record<string, string> = {
778
+ 'Content-Type': contentType,
779
+ };
780
+ if (uploadExtraParams) {
781
+ Object.entries(uploadExtraParams).forEach(([key, value]) => {
782
+ headers[key] = value as string;
783
+ });
784
+ }
785
+
786
+ const resp = await fetch(uploadUrl as any, {
787
+ method: 'PUT',
788
+ headers,
789
+ body: nodeBuffer as any,
790
+ });
791
+
792
+ if (!resp.ok) {
793
+ let bodyText = '';
794
+ try {
795
+ bodyText = await resp.text();
796
+ } catch (e) {
797
+ // ignore
798
+ }
799
+ throw new Error(`Upload failed with status ${resp.status}: ${bodyText}`);
800
+ }
801
+
802
+ const { error: updateError } = await this.adminforth.updateResourceRecord({
803
+ resource: this.resourceConfig,
804
+ recordId,
805
+ oldRecord: existingRecord,
806
+ adminUser,
807
+ extra,
808
+ updates: {
809
+ [this.options.pathColumnName]: filePath,
810
+ },
811
+ } as any);
812
+
813
+ if (updateError) {
814
+ try {
815
+ await this.markKeyForDeletion(filePath);
816
+ } catch (e) {
817
+ // best-effort cleanup, ignore error
818
+ }
819
+ throw new Error(`Error updating record after upload: ${updateError}`);
820
+ }
821
+
667
822
  let previewUrl: string;
668
823
  if (this.options.preview?.previewUrl) {
669
824
  previewUrl = this.options.preview.previewUrl({ filePath });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/upload",
3
- "version": "2.11.0",
3
+ "version": "2.12.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
@@ -199,4 +199,17 @@ export type UploadFromBufferParams = {
199
199
  * Values here do NOT affect the generated storage path.
200
200
  */
201
201
  recordAttributes?: Record<string, any>;
202
+ };
203
+
204
+ /**
205
+ * Parameters for the UploadPlugin.uploadFromBufferToExistingRecord API.
206
+ * Used to upload a binary file buffer and update the path column
207
+ * of an already existing record identified by its primary key.
208
+ */
209
+ export type UploadFromBufferToExistingRecordParams = UploadFromBufferParams & {
210
+ /**
211
+ * Primary key value of the existing record whose file path
212
+ * should be replaced.
213
+ */
214
+ recordId: any;
202
215
  };