@adminforth/upload 2.13.0 → 2.14.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 59,817 bytes received 134 bytes 119,902.00 bytes/sec
14
+ sent 59,824 bytes received 134 bytes 119,916.00 bytes/sec
15
15
  total size is 59,335 speedup is 0.99
package/dist/index.js CHANGED
@@ -10,8 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { AdminForthPlugin, Filters, suggestIfTypo, RateLimiter } from "adminforth";
11
11
  import { Readable } from "stream";
12
12
  import { randomUUID } from "crypto";
13
- import { interpretResource } from 'adminforth';
14
- import { ActionCheckSource } from 'adminforth';
13
+ import { interpretResource, ActionCheckSource } from 'adminforth';
15
14
  const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup';
16
15
  const jobs = new Map();
17
16
  export default class UploadPlugin extends AdminForthPlugin {
@@ -691,4 +690,230 @@ export default class UploadPlugin extends AdminForthPlugin {
691
690
  };
692
691
  });
693
692
  }
693
+ /**
694
+ * Generates a new signed upload URL for future uploading from the frontend via a direct upload (e.g. using fetch + FormData).
695
+ *
696
+ * After the upload, file still will be marked for auto-deletion after short time, so to keep it permanently,
697
+ * you need to either:
698
+ * * Use commitUrlToExistingRecord to commit the URL to an existing record. This will replace the path in the existing record and will do a cleanup of the old
699
+ * file pointed in this path column.
700
+ * * If you want to create a new record with this URL, you can call commitUrlToNewRecord, which will create a new record and set the path column to the uploaded file path.
701
+ * * Write URL to special field called pathColumnName so afterSave hook installed by the plugin will automatically mark as not candidate for auto-deletion
702
+ *
703
+ * ```ts
704
+ * const file = input.files[0];
705
+ *
706
+ * // 1) Ask your backend to call getUploadUrlForExistingRecord
707
+ * const { uploadUrl, filePath, uploadExtraParams } = await fetch('/api/uploads/get-url-existing', {
708
+ * method: 'POST',
709
+ * headers: { 'Content-Type': 'application/json' },
710
+ * body: JSON.stringify({
711
+ * recordId,
712
+ * filename: file.name,
713
+ * contentType: file.type,
714
+ * size: file.size,
715
+ * }),
716
+ * }).then(r => r.json());
717
+ *
718
+ * const formData = new FormData();
719
+ * if (uploadExtraParams) {
720
+ * Object.entries(uploadExtraParams).forEach(([key, value]) => {
721
+ * formData.append(key, value as string);
722
+ * });
723
+ * }
724
+ * formData.append('file', file);
725
+ *
726
+ * // 2) Direct upload from the browser to storage (multipart/form-data)
727
+ * const uploadResp = await fetch(uploadUrl, {
728
+ * method: 'POST',
729
+ * body: formData,
730
+ * });
731
+ * if (!uploadResp.ok) {
732
+ * throw new Error('Upload failed');
733
+ * }
734
+ *
735
+ * // 3) Tell your backend to commit the URL to the record e.g. from rest API call
736
+ * await fetch('/api/uploads/commit-existing', {
737
+ * method: 'POST',
738
+ * headers: { 'Content-Type': 'application/json' },
739
+ * body: JSON.stringify({ recordId, filePath }),
740
+ * });
741
+ * ```
742
+ */
743
+ getUploadUrl(_a) {
744
+ return __awaiter(this, arguments, void 0, function* ({ recordId, filename, contentType, size, }) {
745
+ if (!filename || !contentType) {
746
+ throw new Error('filename and contentType are required');
747
+ }
748
+ if (!this.resourceConfig) {
749
+ throw new Error('resourceConfig is not initialized yet');
750
+ }
751
+ const pkColumn = this.resourceConfig.columns.find((column) => column.primaryKey);
752
+ const pkName = pkColumn === null || pkColumn === void 0 ? void 0 : pkColumn.name;
753
+ if (!pkName) {
754
+ throw new Error('Primary key column not found in resource configuration');
755
+ }
756
+ let existingRecord = undefined;
757
+ if (recordId !== undefined && recordId !== null) {
758
+ existingRecord = yield this.adminforth
759
+ .resource(this.resourceConfig.resourceId)
760
+ .get([Filters.EQ(pkName, recordId)]);
761
+ if (!existingRecord) {
762
+ throw new Error(`Record with id ${recordId} not found`);
763
+ }
764
+ }
765
+ const lastDotIndex = filename.lastIndexOf('.');
766
+ if (lastDotIndex === -1) {
767
+ throw new Error('filename must contain an extension');
768
+ }
769
+ const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
770
+ const originalFilename = filename.substring(0, lastDotIndex);
771
+ if (this.options.allowedFileExtensions &&
772
+ !this.options.allowedFileExtensions.includes(originalExtension)) {
773
+ throw new Error(`File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(', ')}`);
774
+ }
775
+ if (size != null && this.options.maxFileSize && size > this.options.maxFileSize) {
776
+ throw new Error(`File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`);
777
+ }
778
+ const existingValue = existingRecord === null || existingRecord === void 0 ? void 0 : existingRecord[this.options.pathColumnName];
779
+ const existingPaths = existingValue ? this.normalizePaths(existingValue) : undefined;
780
+ const filePath = this.options.filePath({
781
+ originalFilename,
782
+ originalExtension,
783
+ contentType,
784
+ record: existingRecord,
785
+ });
786
+ if (filePath.startsWith('/')) {
787
+ throw new Error('s3Path should not start with /, please adjust s3path function to not return / at the start of the path');
788
+ }
789
+ if (existingPaths && existingPaths.includes(filePath)) {
790
+ throw new Error('New file path cannot be the same as existing path to avoid caching issues');
791
+ }
792
+ const { uploadUrl, uploadExtraParams } = yield this.options.storageAdapter.getUploadSignedUrl(filePath, contentType, 1800);
793
+ return {
794
+ uploadUrl,
795
+ filePath,
796
+ uploadExtraParams,
797
+ pathColumnName: this.options.pathColumnName,
798
+ };
799
+ });
800
+ }
801
+ /**
802
+ * Commits a previously generated upload URL to an existing record.
803
+ *
804
+ * Never call this method from edit afterSave and beforeSave hooks of the same resource,
805
+ * as it would create infinite loop of record updates.
806
+ * You should call this method from your own custom API endpoint after the upload is done.
807
+ */
808
+ commitUrlToUpdateExistingRecord(_a) {
809
+ return __awaiter(this, arguments, void 0, function* ({ recordId, filePath, adminUser, extra, }) {
810
+ var _b;
811
+ if (recordId === undefined || recordId === null) {
812
+ throw new Error('recordId is required');
813
+ }
814
+ if (!filePath) {
815
+ throw new Error('filePath is required');
816
+ }
817
+ if (!this.resourceConfig) {
818
+ throw new Error('resourceConfig is not initialized yet');
819
+ }
820
+ const pkColumn = this.resourceConfig.columns.find((column) => column.primaryKey);
821
+ const pkName = pkColumn === null || pkColumn === void 0 ? void 0 : pkColumn.name;
822
+ if (!pkName) {
823
+ throw new Error('Primary key column not found in resource configuration');
824
+ }
825
+ const existingRecord = yield this.adminforth
826
+ .resource(this.resourceConfig.resourceId)
827
+ .get([Filters.EQ(pkName, recordId)]);
828
+ if (!existingRecord) {
829
+ throw new Error(`Record with id ${recordId} not found`);
830
+ }
831
+ const existingValue = existingRecord[this.options.pathColumnName];
832
+ const existingPaths = this.normalizePaths(existingValue);
833
+ if (existingPaths.includes(filePath)) {
834
+ throw new Error('New file path cannot be the same as existing path to avoid caching issues');
835
+ }
836
+ const { error: updateError } = yield this.adminforth.updateResourceRecord({
837
+ resource: this.resourceConfig,
838
+ recordId,
839
+ oldRecord: existingRecord,
840
+ adminUser,
841
+ extra,
842
+ updates: {
843
+ [this.options.pathColumnName]: filePath,
844
+ },
845
+ });
846
+ if (updateError) {
847
+ try {
848
+ yield this.markKeyForDeletion(filePath);
849
+ }
850
+ catch (e) {
851
+ // best-effort cleanup, ignore error
852
+ }
853
+ throw new Error(`Error updating record after upload: ${updateError}`);
854
+ }
855
+ let previewUrl;
856
+ if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
857
+ previewUrl = this.options.preview.previewUrl({ filePath });
858
+ }
859
+ else {
860
+ previewUrl = yield this.options.storageAdapter.getDownloadUrl(filePath, 1800);
861
+ }
862
+ return {
863
+ path: filePath,
864
+ previewUrl,
865
+ };
866
+ });
867
+ }
868
+ /**
869
+ * Commits a previously generated upload URL to a new record.
870
+ *
871
+ * Never call this method from create afterSave and beforeSave hooks of the same resource,
872
+ * as it would create infinite loop of record creations.
873
+ *
874
+ * You should call this method from your own custom API endpoint after the upload is done.
875
+ */
876
+ commitUrlToNewRecord(_a) {
877
+ return __awaiter(this, arguments, void 0, function* ({ filePath, adminUser, extra, recordAttributes, }) {
878
+ var _b;
879
+ if (!filePath) {
880
+ throw new Error('filePath is required');
881
+ }
882
+ if (!this.resourceConfig) {
883
+ throw new Error('resourceConfig is not initialized yet');
884
+ }
885
+ // Mark this key as used so lifecycle rules do not delete it.
886
+ yield this.markKeyForNotDeletion(filePath);
887
+ const { error: createError, createdRecord, newRecordId } = yield this.adminforth.createResourceRecord({
888
+ resource: this.resourceConfig,
889
+ record: Object.assign(Object.assign({}, (recordAttributes !== null && recordAttributes !== void 0 ? recordAttributes : {})), { [this.options.pathColumnName]: filePath }),
890
+ adminUser,
891
+ extra,
892
+ });
893
+ if (createError) {
894
+ try {
895
+ yield this.markKeyForDeletion(filePath);
896
+ }
897
+ catch (e) {
898
+ // best-effort cleanup, ignore error
899
+ }
900
+ throw new Error(`Error creating record after upload: ${createError}`);
901
+ }
902
+ const pkColumn = this.resourceConfig.columns.find((column) => column.primaryKey);
903
+ const pkName = pkColumn === null || pkColumn === void 0 ? void 0 : pkColumn.name;
904
+ const newRecordPk = newRecordId !== null && newRecordId !== void 0 ? newRecordId : (pkName && createdRecord ? createdRecord[pkName] : undefined);
905
+ let previewUrl;
906
+ if ((_b = this.options.preview) === null || _b === void 0 ? void 0 : _b.previewUrl) {
907
+ previewUrl = this.options.preview.previewUrl({ filePath });
908
+ }
909
+ else {
910
+ previewUrl = yield this.options.storageAdapter.getDownloadUrl(filePath, 1800);
911
+ }
912
+ return {
913
+ path: filePath,
914
+ previewUrl,
915
+ newRecordPk,
916
+ };
917
+ });
918
+ }
694
919
  }
package/index.ts CHANGED
@@ -1,10 +1,16 @@
1
1
 
2
- import { PluginOptions, UploadFromBufferParams, UploadFromBufferToExistingRecordParams } from './types.js';
3
- import { AdminForthPlugin, AdminForthResourceColumn, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo, RateLimiter, AdminUser, HttpExtra } from "adminforth";
2
+ import {
3
+ PluginOptions,
4
+ UploadFromBufferParams,
5
+ UploadFromBufferToExistingRecordParams,
6
+ CommitUrlToUpdateExistingRecordParams,
7
+ CommitUrlToNewRecordParams,
8
+ GetUploadUrlParams,
9
+ } from './types.js';
10
+ import { AdminForthPlugin, AdminForthResource, Filters, IAdminForth, IHttpServer, suggestIfTypo, RateLimiter } from "adminforth";
4
11
  import { Readable } from "stream";
5
12
  import { randomUUID } from "crypto";
6
- import { interpretResource } from 'adminforth';
7
- import { ActionCheckSource } from 'adminforth';
13
+ import { interpretResource, ActionCheckSource } from 'adminforth';
8
14
 
9
15
  const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup';
10
16
  const jobs = new Map();
@@ -806,4 +812,288 @@ export default class UploadPlugin extends AdminForthPlugin {
806
812
  };
807
813
  }
808
814
 
815
+ /**
816
+ * Generates a new signed upload URL for future uploading from the frontend via a direct upload (e.g. using fetch + FormData).
817
+ *
818
+ * After the upload, file still will be marked for auto-deletion after short time, so to keep it permanently,
819
+ * you need to either:
820
+ * * Use commitUrlToExistingRecord to commit the URL to an existing record. This will replace the path in the existing record and will do a cleanup of the old
821
+ * file pointed in this path column.
822
+ * * If you want to create a new record with this URL, you can call commitUrlToNewRecord, which will create a new record and set the path column to the uploaded file path.
823
+ * * Write URL to special field called pathColumnName so afterSave hook installed by the plugin will automatically mark as not candidate for auto-deletion
824
+ *
825
+ * ```ts
826
+ * const file = input.files[0];
827
+ *
828
+ * // 1) Ask your backend to call getUploadUrlForExistingRecord
829
+ * const { uploadUrl, filePath, uploadExtraParams } = await fetch('/api/uploads/get-url-existing', {
830
+ * method: 'POST',
831
+ * headers: { 'Content-Type': 'application/json' },
832
+ * body: JSON.stringify({
833
+ * recordId,
834
+ * filename: file.name,
835
+ * contentType: file.type,
836
+ * size: file.size,
837
+ * }),
838
+ * }).then(r => r.json());
839
+ *
840
+ * const formData = new FormData();
841
+ * if (uploadExtraParams) {
842
+ * Object.entries(uploadExtraParams).forEach(([key, value]) => {
843
+ * formData.append(key, value as string);
844
+ * });
845
+ * }
846
+ * formData.append('file', file);
847
+ *
848
+ * // 2) Direct upload from the browser to storage (multipart/form-data)
849
+ * const uploadResp = await fetch(uploadUrl, {
850
+ * method: 'POST',
851
+ * body: formData,
852
+ * });
853
+ * if (!uploadResp.ok) {
854
+ * throw new Error('Upload failed');
855
+ * }
856
+ *
857
+ * // 3) Tell your backend to commit the URL to the record e.g. from rest API call
858
+ * await fetch('/api/uploads/commit-existing', {
859
+ * method: 'POST',
860
+ * headers: { 'Content-Type': 'application/json' },
861
+ * body: JSON.stringify({ recordId, filePath }),
862
+ * });
863
+ * ```
864
+ */
865
+ async getUploadUrl({
866
+ recordId,
867
+ filename,
868
+ contentType,
869
+ size,
870
+ }: GetUploadUrlParams): Promise<{
871
+ uploadUrl: string;
872
+ filePath: string;
873
+ uploadExtraParams?: Record<string, string>;
874
+ pathColumnName: string;
875
+ }> {
876
+ if (!filename || !contentType) {
877
+ throw new Error('filename and contentType are required');
878
+ }
879
+ if (!this.resourceConfig) {
880
+ throw new Error('resourceConfig is not initialized yet');
881
+ }
882
+
883
+ const pkColumn = this.resourceConfig.columns.find((column: any) => column.primaryKey);
884
+ const pkName = pkColumn?.name;
885
+ if (!pkName) {
886
+ throw new Error('Primary key column not found in resource configuration');
887
+ }
888
+
889
+ let existingRecord: any = undefined;
890
+ if (recordId !== undefined && recordId !== null) {
891
+ existingRecord = await this.adminforth
892
+ .resource(this.resourceConfig.resourceId)
893
+ .get([Filters.EQ(pkName, recordId)]);
894
+
895
+ if (!existingRecord) {
896
+ throw new Error(`Record with id ${recordId} not found`);
897
+ }
898
+ }
899
+
900
+ const lastDotIndex = filename.lastIndexOf('.');
901
+ if (lastDotIndex === -1) {
902
+ throw new Error('filename must contain an extension');
903
+ }
904
+
905
+ const originalExtension = filename.substring(lastDotIndex + 1).toLowerCase();
906
+ const originalFilename = filename.substring(0, lastDotIndex);
907
+
908
+ if (
909
+ this.options.allowedFileExtensions &&
910
+ !this.options.allowedFileExtensions.includes(originalExtension)
911
+ ) {
912
+ throw new Error(
913
+ `File extension "${originalExtension}" is not allowed, allowed extensions are: ${this.options.allowedFileExtensions.join(
914
+ ', ',
915
+ )}`,
916
+ );
917
+ }
918
+
919
+ if (size != null && this.options.maxFileSize && size > this.options.maxFileSize) {
920
+ throw new Error(
921
+ `File size ${size} is too large. Maximum allowed size is ${this.options.maxFileSize}`,
922
+ );
923
+ }
924
+
925
+ const existingValue = existingRecord?.[this.options.pathColumnName];
926
+ const existingPaths = existingValue ? this.normalizePaths(existingValue) : undefined;
927
+
928
+ const filePath: string = this.options.filePath({
929
+ originalFilename,
930
+ originalExtension,
931
+ contentType,
932
+ record: existingRecord,
933
+ });
934
+
935
+ if (filePath.startsWith('/')) {
936
+ throw new Error(
937
+ 's3Path should not start with /, please adjust s3path function to not return / at the start of the path',
938
+ );
939
+ }
940
+
941
+ if (existingPaths && existingPaths.includes(filePath)) {
942
+ throw new Error(
943
+ 'New file path cannot be the same as existing path to avoid caching issues',
944
+ );
945
+ }
946
+
947
+ const { uploadUrl, uploadExtraParams } = await this.options.storageAdapter.getUploadSignedUrl(
948
+ filePath,
949
+ contentType,
950
+ 1800,
951
+ );
952
+
953
+ return {
954
+ uploadUrl,
955
+ filePath,
956
+ uploadExtraParams,
957
+ pathColumnName: this.options.pathColumnName,
958
+ };
959
+ }
960
+
961
+ /**
962
+ * Commits a previously generated upload URL to an existing record.
963
+ *
964
+ * Never call this method from edit afterSave and beforeSave hooks of the same resource,
965
+ * as it would create infinite loop of record updates.
966
+ * You should call this method from your own custom API endpoint after the upload is done.
967
+ */
968
+ async commitUrlToUpdateExistingRecord({
969
+ recordId,
970
+ filePath,
971
+ adminUser,
972
+ extra,
973
+ }: CommitUrlToUpdateExistingRecordParams): Promise<{ path: string; previewUrl: string }> {
974
+ if (recordId === undefined || recordId === null) {
975
+ throw new Error('recordId is required');
976
+ }
977
+ if (!filePath) {
978
+ throw new Error('filePath is required');
979
+ }
980
+ if (!this.resourceConfig) {
981
+ throw new Error('resourceConfig is not initialized yet');
982
+ }
983
+
984
+ const pkColumn = this.resourceConfig.columns.find((column: any) => column.primaryKey);
985
+ const pkName = pkColumn?.name;
986
+ if (!pkName) {
987
+ throw new Error('Primary key column not found in resource configuration');
988
+ }
989
+
990
+ const existingRecord = await this.adminforth
991
+ .resource(this.resourceConfig.resourceId)
992
+ .get([Filters.EQ(pkName, recordId)]);
993
+
994
+ if (!existingRecord) {
995
+ throw new Error(`Record with id ${recordId} not found`);
996
+ }
997
+
998
+ const existingValue = existingRecord[this.options.pathColumnName];
999
+ const existingPaths = this.normalizePaths(existingValue);
1000
+
1001
+ if (existingPaths.includes(filePath)) {
1002
+ throw new Error(
1003
+ 'New file path cannot be the same as existing path to avoid caching issues',
1004
+ );
1005
+ }
1006
+
1007
+ const { error: updateError } = await this.adminforth.updateResourceRecord({
1008
+ resource: this.resourceConfig,
1009
+ recordId,
1010
+ oldRecord: existingRecord,
1011
+ adminUser,
1012
+ extra,
1013
+ updates: {
1014
+ [this.options.pathColumnName]: filePath,
1015
+ },
1016
+ } as any);
1017
+
1018
+ if (updateError) {
1019
+ try {
1020
+ await this.markKeyForDeletion(filePath);
1021
+ } catch (e) {
1022
+ // best-effort cleanup, ignore error
1023
+ }
1024
+ throw new Error(`Error updating record after upload: ${updateError}`);
1025
+ }
1026
+
1027
+ let previewUrl: string;
1028
+ if (this.options.preview?.previewUrl) {
1029
+ previewUrl = this.options.preview.previewUrl({ filePath });
1030
+ } else {
1031
+ previewUrl = await this.options.storageAdapter.getDownloadUrl(filePath, 1800);
1032
+ }
1033
+
1034
+ return {
1035
+ path: filePath,
1036
+ previewUrl,
1037
+ };
1038
+ }
1039
+
1040
+ /**
1041
+ * Commits a previously generated upload URL to a new record.
1042
+ *
1043
+ * Never call this method from create afterSave and beforeSave hooks of the same resource,
1044
+ * as it would create infinite loop of record creations.
1045
+ *
1046
+ * You should call this method from your own custom API endpoint after the upload is done.
1047
+ */
1048
+ async commitUrlToNewRecord({
1049
+ filePath,
1050
+ adminUser,
1051
+ extra,
1052
+ recordAttributes,
1053
+ }: CommitUrlToNewRecordParams): Promise<{ path: string; previewUrl: string; newRecordPk: any }> {
1054
+ if (!filePath) {
1055
+ throw new Error('filePath is required');
1056
+ }
1057
+ if (!this.resourceConfig) {
1058
+ throw new Error('resourceConfig is not initialized yet');
1059
+ }
1060
+
1061
+ // Mark this key as used so lifecycle rules do not delete it.
1062
+ await this.markKeyForNotDeletion(filePath);
1063
+
1064
+ const { error: createError, createdRecord, newRecordId }: any =
1065
+ await this.adminforth.createResourceRecord({
1066
+ resource: this.resourceConfig,
1067
+ record: { ...(recordAttributes ?? {}), [this.options.pathColumnName]: filePath },
1068
+ adminUser,
1069
+ extra,
1070
+ });
1071
+
1072
+ if (createError) {
1073
+ try {
1074
+ await this.markKeyForDeletion(filePath);
1075
+ } catch (e) {
1076
+ // best-effort cleanup, ignore error
1077
+ }
1078
+ throw new Error(`Error creating record after upload: ${createError}`);
1079
+ }
1080
+
1081
+ const pkColumn = this.resourceConfig.columns.find((column: any) => column.primaryKey);
1082
+ const pkName = pkColumn?.name;
1083
+ const newRecordPk = newRecordId ?? (pkName && createdRecord ? createdRecord[pkName] : undefined);
1084
+
1085
+ let previewUrl: string;
1086
+ if (this.options.preview?.previewUrl) {
1087
+ previewUrl = this.options.preview.previewUrl({ filePath });
1088
+ } else {
1089
+ previewUrl = await this.options.storageAdapter.getDownloadUrl(filePath, 1800);
1090
+ }
1091
+
1092
+ return {
1093
+ path: filePath,
1094
+ previewUrl,
1095
+ newRecordPk,
1096
+ };
1097
+ }
1098
+
809
1099
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/upload",
3
- "version": "2.13.0",
3
+ "version": "2.14.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
@@ -212,4 +212,107 @@ export type UploadFromBufferToExistingRecordParams = UploadFromBufferParams & {
212
212
  * should be replaced.
213
213
  */
214
214
  recordId: any;
215
+ };
216
+
217
+ /**
218
+ * Parameters for generating an upload URL for an existing record
219
+ * that will be uploaded directly from the browser.
220
+ */
221
+ export type GetUploadUrlParams = {
222
+ /**
223
+ * Primary key of the record whose file is being replaced.
224
+ */
225
+ recordId?: any;
226
+
227
+ /**
228
+ * Full file name including extension, as provided by the browser.
229
+ */
230
+ filename: string;
231
+
232
+ /**
233
+ * MIME type reported by the browser, used as Content-Type for storage.
234
+ */
235
+ contentType: string;
236
+
237
+ /**
238
+ * Optional file size in bytes. If provided, it will be validated
239
+ * against {@link PluginOptions.maxFileSize}.
240
+ */
241
+ size?: number;
242
+ };
243
+
244
+ /**
245
+ * Parameters for generating an upload URL for a new record
246
+ * that will be uploaded directly from the browser.
247
+ */
248
+ export type GetUploadUrlForNewRecordParams = {
249
+ /**
250
+ * Full file name including extension, as provided by the browser.
251
+ */
252
+ filename: string;
253
+
254
+ /**
255
+ * MIME type reported by the browser, used as Content-Type for storage.
256
+ */
257
+ contentType: string;
258
+
259
+ /**
260
+ * Optional file size in bytes. If provided, it will be validated
261
+ * against {@link PluginOptions.maxFileSize}.
262
+ */
263
+ size?: number;
264
+ };
265
+
266
+ /**
267
+ * Parameters for committing a previously generated upload URL
268
+ * to an existing record. This is used after the browser finished
269
+ * uploading directly to the storage provider.
270
+ */
271
+ export type CommitUrlToUpdateExistingRecordParams = {
272
+ /**
273
+ * Primary key of the record whose file is being replaced.
274
+ */
275
+ recordId: any;
276
+
277
+ /**
278
+ * Storage path (key) that was returned by getUploadUrlForExistingRecord.
279
+ */
280
+ filePath: string;
281
+
282
+ /**
283
+ * Authenticated admin user on whose behalf the record is updated.
284
+ */
285
+ adminUser: AdminUser;
286
+
287
+ /**
288
+ * Optional HTTP context (headers, IP, etc.).
289
+ */
290
+ extra?: HttpExtra;
291
+ };
292
+
293
+ /**
294
+ * Parameters for committing a previously generated upload URL
295
+ * to a new record. This is used after the browser finished
296
+ * uploading directly to the storage provider.
297
+ */
298
+ export type CommitUrlToNewRecordParams = {
299
+ /**
300
+ * Storage path (key) that was returned by getUploadUrlForNewRecord.
301
+ */
302
+ filePath: string;
303
+
304
+ /**
305
+ * Authenticated admin user on whose behalf the record is created.
306
+ */
307
+ adminUser: AdminUser;
308
+
309
+ /**
310
+ * Optional HTTP context (headers, IP, etc.).
311
+ */
312
+ extra?: HttpExtra;
313
+
314
+ /**
315
+ * Optional additional attributes for the new record.
316
+ */
317
+ recordAttributes?: Record<string, any>;
215
318
  };