@opensaas/stack-storage 0.1.1

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.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +11 -0
  3. package/CLAUDE.md +426 -0
  4. package/LICENSE +21 -0
  5. package/README.md +425 -0
  6. package/dist/config/index.d.ts +19 -0
  7. package/dist/config/index.d.ts.map +1 -0
  8. package/dist/config/index.js +25 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/config/types.d.ts +113 -0
  11. package/dist/config/types.d.ts.map +1 -0
  12. package/dist/config/types.js +2 -0
  13. package/dist/config/types.js.map +1 -0
  14. package/dist/fields/index.d.ts +111 -0
  15. package/dist/fields/index.d.ts.map +1 -0
  16. package/dist/fields/index.js +237 -0
  17. package/dist/fields/index.js.map +1 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +11 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/providers/index.d.ts +2 -0
  23. package/dist/providers/index.d.ts.map +1 -0
  24. package/dist/providers/index.js +2 -0
  25. package/dist/providers/index.js.map +1 -0
  26. package/dist/providers/local.d.ts +22 -0
  27. package/dist/providers/local.d.ts.map +1 -0
  28. package/dist/providers/local.js +64 -0
  29. package/dist/providers/local.js.map +1 -0
  30. package/dist/runtime/index.d.ts +75 -0
  31. package/dist/runtime/index.d.ts.map +1 -0
  32. package/dist/runtime/index.js +157 -0
  33. package/dist/runtime/index.js.map +1 -0
  34. package/dist/utils/image.d.ts +18 -0
  35. package/dist/utils/image.d.ts.map +1 -0
  36. package/dist/utils/image.js +82 -0
  37. package/dist/utils/image.js.map +1 -0
  38. package/dist/utils/index.d.ts +3 -0
  39. package/dist/utils/index.d.ts.map +1 -0
  40. package/dist/utils/index.js +3 -0
  41. package/dist/utils/index.js.map +1 -0
  42. package/dist/utils/upload.d.ts +56 -0
  43. package/dist/utils/upload.d.ts.map +1 -0
  44. package/dist/utils/upload.js +74 -0
  45. package/dist/utils/upload.js.map +1 -0
  46. package/package.json +50 -0
  47. package/src/config/index.ts +30 -0
  48. package/src/config/types.ts +127 -0
  49. package/src/fields/index.ts +347 -0
  50. package/src/index.ts +14 -0
  51. package/src/providers/index.ts +1 -0
  52. package/src/providers/local.ts +85 -0
  53. package/src/runtime/index.ts +243 -0
  54. package/src/utils/image.ts +111 -0
  55. package/src/utils/index.ts +2 -0
  56. package/src/utils/upload.ts +122 -0
  57. package/tests/image-utils.test.ts +498 -0
  58. package/tests/local-provider.test.ts +349 -0
  59. package/tests/upload-utils.test.ts +313 -0
  60. package/tsconfig.json +9 -0
  61. package/vitest.config.ts +14 -0
@@ -0,0 +1,157 @@
1
+ import { LocalStorageProvider } from '../providers/local.js';
2
+ import { validateFile, getMimeType } from '../utils/upload.js';
3
+ import { getImageDimensions, processImageTransformations } from '../utils/image.js';
4
+ /**
5
+ * Creates a storage provider instance from config
6
+ */
7
+ export function createStorageProvider(config, providerName) {
8
+ if (!config.storage || !config.storage[providerName]) {
9
+ throw new Error(`Storage provider '${providerName}' not found in config`);
10
+ }
11
+ const providerConfig = config.storage[providerName];
12
+ switch (providerConfig.type) {
13
+ case 'local':
14
+ return new LocalStorageProvider(providerConfig);
15
+ default:
16
+ throw new Error(`Unknown storage provider type: ${providerConfig.type}`);
17
+ }
18
+ }
19
+ /**
20
+ * Uploads a file to the specified storage provider
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const metadata = await uploadFile(config, 'documents', {
25
+ * file,
26
+ * buffer,
27
+ * validation: {
28
+ * maxFileSize: 10 * 1024 * 1024, // 10MB
29
+ * acceptedMimeTypes: ['application/pdf']
30
+ * }
31
+ * })
32
+ * ```
33
+ */
34
+ export async function uploadFile(config, storageProviderName, data, options) {
35
+ const { file, buffer } = data;
36
+ // Validate file
37
+ if (options?.validation) {
38
+ const validation = validateFile({
39
+ size: file.size,
40
+ name: file.name,
41
+ type: file.type,
42
+ }, options.validation);
43
+ if (!validation.valid) {
44
+ throw new Error(validation.error);
45
+ }
46
+ }
47
+ // Get storage provider
48
+ const provider = createStorageProvider(config, storageProviderName);
49
+ // Determine content type
50
+ const contentType = file.type || getMimeType(file.name);
51
+ // Upload file
52
+ const result = await provider.upload(buffer, file.name, {
53
+ contentType,
54
+ metadata: options?.metadata,
55
+ });
56
+ // Return metadata
57
+ return {
58
+ filename: result.filename,
59
+ originalFilename: file.name,
60
+ url: result.url,
61
+ mimeType: contentType,
62
+ size: result.size,
63
+ uploadedAt: new Date().toISOString(),
64
+ storageProvider: storageProviderName,
65
+ metadata: result.metadata,
66
+ };
67
+ }
68
+ /**
69
+ * Uploads an image with optional transformations
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const metadata = await uploadImage(config, 'avatars', {
74
+ * file,
75
+ * buffer,
76
+ * validation: {
77
+ * maxFileSize: 5 * 1024 * 1024, // 5MB
78
+ * acceptedMimeTypes: ['image/jpeg', 'image/png', 'image/webp']
79
+ * },
80
+ * transformations: {
81
+ * thumbnail: { width: 100, height: 100, fit: 'cover' },
82
+ * profile: { width: 400, height: 400, fit: 'cover' }
83
+ * }
84
+ * })
85
+ * ```
86
+ */
87
+ export async function uploadImage(config, storageProviderName, data, options) {
88
+ const { file, buffer } = data;
89
+ // Validate file
90
+ if (options?.validation) {
91
+ const validation = validateFile({
92
+ size: file.size,
93
+ name: file.name,
94
+ type: file.type,
95
+ }, options.validation);
96
+ if (!validation.valid) {
97
+ throw new Error(validation.error);
98
+ }
99
+ }
100
+ // Get storage provider
101
+ const provider = createStorageProvider(config, storageProviderName);
102
+ // Determine content type
103
+ const contentType = file.type || getMimeType(file.name);
104
+ // Get original image dimensions
105
+ const { width, height } = await getImageDimensions(buffer);
106
+ // Upload original image
107
+ const result = await provider.upload(buffer, file.name, {
108
+ contentType,
109
+ metadata: options?.metadata,
110
+ });
111
+ // Process transformations if provided
112
+ let transformations;
113
+ if (options?.transformations) {
114
+ transformations = await processImageTransformations(buffer, file.name, options.transformations, provider, contentType);
115
+ }
116
+ // Return metadata
117
+ return {
118
+ filename: result.filename,
119
+ originalFilename: file.name,
120
+ url: result.url,
121
+ mimeType: contentType,
122
+ size: result.size,
123
+ width,
124
+ height,
125
+ uploadedAt: new Date().toISOString(),
126
+ storageProvider: storageProviderName,
127
+ metadata: result.metadata,
128
+ transformations,
129
+ };
130
+ }
131
+ /**
132
+ * Deletes a file from storage
133
+ */
134
+ export async function deleteFile(config, storageProviderName, filename) {
135
+ const provider = createStorageProvider(config, storageProviderName);
136
+ await provider.delete(filename);
137
+ }
138
+ /**
139
+ * Deletes an image and all its transformations from storage
140
+ */
141
+ export async function deleteImage(config, metadata) {
142
+ const provider = createStorageProvider(config, metadata.storageProvider);
143
+ // Delete original image
144
+ await provider.delete(metadata.filename);
145
+ // Delete all transformations
146
+ if (metadata.transformations) {
147
+ for (const transformationResult of Object.values(metadata.transformations)) {
148
+ // Extract filename from URL
149
+ const filename = transformationResult.url.split('/').pop();
150
+ if (filename) {
151
+ await provider.delete(filename);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ export { parseFileFromFormData } from '../utils/upload.js';
157
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { ImageTransformationConfig, ImageTransformationResult, StorageProvider } from '../config/types.js';
2
+ /**
3
+ * Gets image dimensions from a buffer
4
+ */
5
+ export declare function getImageDimensions(buffer: Buffer | Uint8Array): Promise<{
6
+ width: number;
7
+ height: number;
8
+ }>;
9
+ /**
10
+ * Applies a single transformation to an image
11
+ */
12
+ export declare function transformImage(buffer: Buffer | Uint8Array, transformation: ImageTransformationConfig): Promise<Buffer>;
13
+ /**
14
+ * Processes all transformations for an image
15
+ * Uploads the original and all transformed versions
16
+ */
17
+ export declare function processImageTransformations(buffer: Buffer | Uint8Array, originalFilename: string, transformations: Record<string, ImageTransformationConfig>, storageProvider: StorageProvider, contentType: string): Promise<Record<string, ImageTransformationResult>>;
18
+ //# sourceMappingURL=image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/utils/image.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,eAAe,EAChB,MAAM,oBAAoB,CAAA;AAE3B;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAM5C;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,cAAc,EAAE,yBAAyB,GACxC,OAAO,CAAC,MAAM,CAAC,CAmCjB;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,gBAAgB,EAAE,MAAM,EACxB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,EAC1D,eAAe,EAAE,eAAe,EAChC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAqCpD"}
@@ -0,0 +1,82 @@
1
+ import sharp from 'sharp';
2
+ /**
3
+ * Gets image dimensions from a buffer
4
+ */
5
+ export async function getImageDimensions(buffer) {
6
+ const metadata = await sharp(buffer).metadata();
7
+ return {
8
+ width: metadata.width || 0,
9
+ height: metadata.height || 0,
10
+ };
11
+ }
12
+ /**
13
+ * Applies a single transformation to an image
14
+ */
15
+ export async function transformImage(buffer, transformation) {
16
+ let image = sharp(buffer);
17
+ // Apply resizing
18
+ if (transformation.width || transformation.height) {
19
+ image = image.resize({
20
+ width: transformation.width,
21
+ height: transformation.height,
22
+ fit: transformation.fit || 'cover',
23
+ });
24
+ }
25
+ // Apply format conversion
26
+ if (transformation.format) {
27
+ const options = {
28
+ quality: transformation.quality || 80,
29
+ };
30
+ switch (transformation.format) {
31
+ case 'jpeg':
32
+ image = image.jpeg(options);
33
+ break;
34
+ case 'png':
35
+ image = image.png(options);
36
+ break;
37
+ case 'webp':
38
+ image = image.webp(options);
39
+ break;
40
+ case 'avif':
41
+ image = image.avif(options);
42
+ break;
43
+ }
44
+ }
45
+ return await image.toBuffer();
46
+ }
47
+ /**
48
+ * Processes all transformations for an image
49
+ * Uploads the original and all transformed versions
50
+ */
51
+ export async function processImageTransformations(buffer, originalFilename, transformations, storageProvider, contentType) {
52
+ const results = {};
53
+ for (const [name, config] of Object.entries(transformations)) {
54
+ // Transform the image
55
+ const transformedBuffer = await transformImage(buffer, config);
56
+ // Get dimensions of transformed image
57
+ const { width, height } = await getImageDimensions(transformedBuffer);
58
+ // Generate filename for transformation
59
+ const ext = config.format ? `.${config.format}` : '';
60
+ const transformedFilename = `${originalFilename}-${name}${ext}`;
61
+ // Upload transformed image
62
+ const uploadResult = await storageProvider.upload(transformedBuffer, transformedFilename, {
63
+ contentType: config.format === 'jpeg'
64
+ ? 'image/jpeg'
65
+ : config.format === 'png'
66
+ ? 'image/png'
67
+ : config.format === 'webp'
68
+ ? 'image/webp'
69
+ : config.format === 'avif'
70
+ ? 'image/avif'
71
+ : contentType,
72
+ });
73
+ results[name] = {
74
+ url: uploadResult.url,
75
+ width,
76
+ height,
77
+ size: uploadResult.size,
78
+ };
79
+ }
80
+ return results;
81
+ }
82
+ //# sourceMappingURL=image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/utils/image.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export * from './image.js';
2
+ export * from './upload.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA"}
@@ -0,0 +1,3 @@
1
+ export * from './image.js';
2
+ export * from './upload.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * File validation options
3
+ */
4
+ export interface FileValidationOptions {
5
+ /** Maximum file size in bytes */
6
+ maxFileSize?: number;
7
+ /** Accepted MIME types (e.g., ['image/jpeg', 'image/png']) */
8
+ acceptedMimeTypes?: string[];
9
+ /** Accepted file extensions (e.g., ['.jpg', '.png']) */
10
+ acceptedExtensions?: string[];
11
+ }
12
+ /**
13
+ * File validation result
14
+ */
15
+ export interface FileValidationResult {
16
+ valid: boolean;
17
+ error?: string;
18
+ }
19
+ /**
20
+ * Validates a file against the provided options
21
+ */
22
+ export declare function validateFile(file: {
23
+ size: number;
24
+ name: string;
25
+ type: string;
26
+ }, options?: FileValidationOptions): FileValidationResult;
27
+ /**
28
+ * Formats file size in human-readable format
29
+ */
30
+ export declare function formatFileSize(bytes: number): string;
31
+ /**
32
+ * Gets MIME type from filename
33
+ */
34
+ export declare function getMimeType(filename: string): string;
35
+ /**
36
+ * Extracts file metadata from a File or Blob
37
+ */
38
+ export interface FileInfo {
39
+ name: string;
40
+ size: number;
41
+ type: string;
42
+ lastModified?: number;
43
+ }
44
+ /**
45
+ * Converts a File/Blob to Buffer for Node.js processing
46
+ */
47
+ export declare function fileToBuffer(file: Blob | File): Promise<Buffer>;
48
+ /**
49
+ * Parses FormData and extracts file information
50
+ * This is a utility for developers to use in their upload routes
51
+ */
52
+ export declare function parseFileFromFormData(formData: FormData, fieldName?: string): Promise<{
53
+ file: File;
54
+ buffer: Buffer;
55
+ } | null>;
56
+ //# sourceMappingURL=upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/utils/upload.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8DAA8D;IAC9D,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAClD,OAAO,CAAC,EAAE,qBAAqB,GAC9B,oBAAoB,CAoCtB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrE;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,QAAQ,EAClB,SAAS,GAAE,MAAe,GACzB,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAUhD"}
@@ -0,0 +1,74 @@
1
+ import mime from 'mime-types';
2
+ /**
3
+ * Validates a file against the provided options
4
+ */
5
+ export function validateFile(file, options) {
6
+ if (!options) {
7
+ return { valid: true };
8
+ }
9
+ // Check file size
10
+ if (options.maxFileSize && file.size > options.maxFileSize) {
11
+ return {
12
+ valid: false,
13
+ error: `File size exceeds maximum allowed size of ${formatFileSize(options.maxFileSize)}`,
14
+ };
15
+ }
16
+ // Check MIME type
17
+ if (options.acceptedMimeTypes && options.acceptedMimeTypes.length > 0) {
18
+ const fileMimeType = file.type || mime.lookup(file.name) || '';
19
+ if (!options.acceptedMimeTypes.includes(fileMimeType)) {
20
+ return {
21
+ valid: false,
22
+ error: `File type '${fileMimeType}' is not allowed. Accepted types: ${options.acceptedMimeTypes.join(', ')}`,
23
+ };
24
+ }
25
+ }
26
+ // Check file extension
27
+ if (options.acceptedExtensions && options.acceptedExtensions.length > 0) {
28
+ const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
29
+ if (!options.acceptedExtensions.includes(ext)) {
30
+ return {
31
+ valid: false,
32
+ error: `File extension '${ext}' is not allowed. Accepted extensions: ${options.acceptedExtensions.join(', ')}`,
33
+ };
34
+ }
35
+ }
36
+ return { valid: true };
37
+ }
38
+ /**
39
+ * Formats file size in human-readable format
40
+ */
41
+ export function formatFileSize(bytes) {
42
+ if (bytes === 0)
43
+ return '0 Bytes';
44
+ const k = 1024;
45
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
46
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
47
+ return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
48
+ }
49
+ /**
50
+ * Gets MIME type from filename
51
+ */
52
+ export function getMimeType(filename) {
53
+ return mime.lookup(filename) || 'application/octet-stream';
54
+ }
55
+ /**
56
+ * Converts a File/Blob to Buffer for Node.js processing
57
+ */
58
+ export async function fileToBuffer(file) {
59
+ const arrayBuffer = await file.arrayBuffer();
60
+ return Buffer.from(arrayBuffer);
61
+ }
62
+ /**
63
+ * Parses FormData and extracts file information
64
+ * This is a utility for developers to use in their upload routes
65
+ */
66
+ export async function parseFileFromFormData(formData, fieldName = 'file') {
67
+ const file = formData.get(fieldName);
68
+ if (!file || !(file instanceof File)) {
69
+ return null;
70
+ }
71
+ const buffer = await fileToBuffer(file);
72
+ return { file, buffer };
73
+ }
74
+ //# sourceMappingURL=upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/utils/upload.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@opensaas/stack-storage",
3
+ "version": "0.1.1",
4
+ "description": "File and image upload field types with pluggable storage providers for OpenSaas Stack",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./fields": {
12
+ "types": "./dist/fields/index.d.ts",
13
+ "default": "./dist/fields/index.js"
14
+ },
15
+ "./providers": {
16
+ "types": "./dist/providers/index.d.ts",
17
+ "default": "./dist/providers/index.js"
18
+ },
19
+ "./runtime": {
20
+ "types": "./dist/runtime/index.d.ts",
21
+ "default": "./dist/runtime/index.js"
22
+ }
23
+ },
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "dependencies": {
27
+ "mime-types": "^3.0.1",
28
+ "sharp": "^0.34.4",
29
+ "zod": "^4.1.12",
30
+ "@opensaas/stack-core": "0.1.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/mime-types": "^3.0.1",
34
+ "@types/node": "^24.0.0",
35
+ "@types/react": "^19.2.2",
36
+ "@vitest/coverage-v8": "^4.0.5",
37
+ "typescript": "^5.9.3",
38
+ "vitest": "^4.0.5"
39
+ },
40
+ "peerDependencies": {
41
+ "@opensaas/stack-core": "0.1.1"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc",
45
+ "dev": "tsc --watch",
46
+ "test": "vitest",
47
+ "test:ui": "vitest --ui",
48
+ "test:coverage": "vitest --coverage"
49
+ }
50
+ }
@@ -0,0 +1,30 @@
1
+ import type { LocalStorageConfig } from './types.js'
2
+
3
+ export * from './types.js'
4
+
5
+ /**
6
+ * Creates a local filesystem storage configuration
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const config = config({
11
+ * storage: {
12
+ * documents: localStorage({
13
+ * uploadDir: './uploads/documents',
14
+ * serveUrl: '/api/files',
15
+ * }),
16
+ * },
17
+ * })
18
+ * ```
19
+ */
20
+ export function localStorage(
21
+ config: Pick<LocalStorageConfig, 'uploadDir' | 'serveUrl'> &
22
+ Partial<Pick<LocalStorageConfig, 'generateUniqueFilenames'>>,
23
+ ): LocalStorageConfig {
24
+ return {
25
+ type: 'local',
26
+ uploadDir: config.uploadDir,
27
+ serveUrl: config.serveUrl,
28
+ generateUniqueFilenames: config.generateUniqueFilenames ?? true,
29
+ }
30
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Storage provider interface that all storage backends must implement.
3
+ * This allows pluggable storage solutions (local, S3, Vercel Blob, etc.)
4
+ */
5
+ export interface StorageProvider {
6
+ /**
7
+ * Uploads a file to the storage provider
8
+ * @param file - File data as Buffer or Uint8Array
9
+ * @param filename - Desired filename (may be transformed by provider)
10
+ * @param options - Additional upload options (contentType, metadata, etc.)
11
+ * @returns Upload result with URL and metadata
12
+ */
13
+ upload(
14
+ file: Buffer | Uint8Array,
15
+ filename: string,
16
+ options?: UploadOptions,
17
+ ): Promise<UploadResult>
18
+
19
+ /**
20
+ * Downloads a file from the storage provider
21
+ * @param filename - Filename to download
22
+ * @returns File data as Buffer
23
+ */
24
+ download(filename: string): Promise<Buffer>
25
+
26
+ /**
27
+ * Deletes a file from the storage provider
28
+ * @param filename - Filename to delete
29
+ */
30
+ delete(filename: string): Promise<void>
31
+
32
+ /**
33
+ * Gets the public URL for a file
34
+ * @param filename - Filename to get URL for
35
+ * @returns Public URL string
36
+ */
37
+ getUrl(filename: string): string
38
+
39
+ /**
40
+ * Optional: Gets a signed URL for private files
41
+ * @param filename - Filename to get signed URL for
42
+ * @param expiresIn - Expiration time in seconds
43
+ * @returns Signed URL string
44
+ */
45
+ getSignedUrl?(filename: string, expiresIn?: number): Promise<string>
46
+ }
47
+
48
+ /**
49
+ * Options for uploading a file
50
+ */
51
+ export interface UploadOptions {
52
+ /** MIME type of the file */
53
+ contentType?: string
54
+ /** Custom metadata to store with the file */
55
+ metadata?: Record<string, string>
56
+ /** Whether the file should be publicly accessible */
57
+ public?: boolean
58
+ /** Cache control header */
59
+ cacheControl?: string
60
+ }
61
+
62
+ /**
63
+ * Result from uploading a file
64
+ */
65
+ export interface UploadResult {
66
+ /** Generated filename (may differ from input) */
67
+ filename: string
68
+ /** Public URL to access the file */
69
+ url: string
70
+ /** File size in bytes */
71
+ size: number
72
+ /** MIME type */
73
+ contentType: string
74
+ /** Additional provider-specific metadata */
75
+ metadata?: Record<string, unknown>
76
+ }
77
+
78
+ /**
79
+ * Configuration for local filesystem storage
80
+ */
81
+ export interface LocalStorageConfig {
82
+ type: 'local'
83
+ /** Directory to store uploaded files */
84
+ uploadDir: string
85
+ /** Base URL for serving files (e.g., '/api/files' or 'https://cdn.example.com') */
86
+ serveUrl: string
87
+ /** Whether to generate unique filenames (default: true) */
88
+ generateUniqueFilenames?: boolean
89
+ /** Allow additional properties */
90
+ [key: string]: unknown
91
+ }
92
+
93
+ /**
94
+ * Base configuration shared by all storage providers
95
+ */
96
+ export interface BaseStorageConfig {
97
+ type: string
98
+ [key: string]: unknown
99
+ }
100
+
101
+ /**
102
+ * Storage configuration - maps names to storage provider configs
103
+ * Example: { avatars: s3Config, documents: localConfig }
104
+ */
105
+ export type StorageConfig = Record<string, BaseStorageConfig | LocalStorageConfig>
106
+
107
+ /**
108
+ * Re-export metadata types from core package
109
+ * These types are now defined in @opensaas/stack-core to avoid circular dependencies
110
+ */
111
+ export type { FileMetadata, ImageMetadata, ImageTransformationResult } from '@opensaas/stack-core'
112
+
113
+ /**
114
+ * Configuration for image transformations
115
+ */
116
+ export interface ImageTransformationConfig {
117
+ /** Target width in pixels */
118
+ width?: number
119
+ /** Target height in pixels */
120
+ height?: number
121
+ /** Fit mode: cover (crop), contain (letterbox), fill (stretch) */
122
+ fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'
123
+ /** Output format (default: original format) */
124
+ format?: 'jpeg' | 'png' | 'webp' | 'avif'
125
+ /** Quality 1-100 (default: 80) */
126
+ quality?: number
127
+ }