@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,4 @@
1
+
2
+ > @opensaas/stack-storage@0.1.1 build /home/runner/work/stack/stack/packages/storage
3
+ > tsc
4
+
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @opensaas/stack-storage
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 045c071: Add field and image upload
8
+ - Updated dependencies [9a3fda5]
9
+ - Updated dependencies [f8ebc0e]
10
+ - Updated dependencies [045c071]
11
+ - @opensaas/stack-core@0.1.1
package/CLAUDE.md ADDED
@@ -0,0 +1,426 @@
1
+ # @opensaas/stack-storage
2
+
3
+ File and image upload field types with pluggable storage providers.
4
+
5
+ ## Purpose
6
+
7
+ Provides file and image upload capabilities for OpenSaas Stack with:
8
+
9
+ - Self-contained field types (`file()` and `image()`)
10
+ - Pluggable storage providers (local, S3, Vercel Blob)
11
+ - Automatic image transformations with sharp
12
+ - JSON-backed metadata storage
13
+ - Developer-controlled upload routes
14
+
15
+ ## Package Structure
16
+
17
+ ```
18
+ packages/storage/
19
+ ├── src/
20
+ │ ├── config/ # Storage config types and builders
21
+ │ ├── fields/ # file() and image() field builders
22
+ │ ├── providers/ # LocalStorageProvider
23
+ │ ├── runtime/ # Upload/delete utilities for developers
24
+ │ └── utils/ # Image processing, validation utilities
25
+ └── package.json
26
+
27
+ packages/storage-s3/ # Separate S3 provider package
28
+ packages/storage-vercel/ # Separate Vercel Blob provider package
29
+ ```
30
+
31
+ ## Key Exports
32
+
33
+ ### Config (`src/config/`)
34
+
35
+ - `localStorage(config)` - Creates local filesystem storage config
36
+ - Types: `StorageProvider`, `StorageConfig`, `FileMetadata`, `ImageMetadata`
37
+
38
+ ### Fields (`src/fields/`)
39
+
40
+ - `file(options)` - File upload field builder
41
+ - `image(options)` - Image upload field builder with transformations
42
+
43
+ ### Providers (`src/providers/`)
44
+
45
+ - `LocalStorageProvider` - Built-in filesystem storage
46
+
47
+ ### Runtime (`src/runtime/`)
48
+
49
+ - `uploadFile(config, provider, data, options)` - Upload file and return metadata
50
+ - `uploadImage(config, provider, data, options)` - Upload image with transformations
51
+ - `deleteFile(config, provider, filename)` - Delete file
52
+ - `deleteImage(config, metadata)` - Delete image and all transformations
53
+ - `createStorageProvider(config, providerName)` - Create provider instance
54
+
55
+ ### Utils (`src/utils/`)
56
+
57
+ - `validateFile(file, options)` - Validate file size, MIME type, extensions
58
+ - `formatFileSize(bytes)` - Human-readable file sizes
59
+ - `getMimeType(filename)` - Get MIME type from filename
60
+ - `parseFileFromFormData(formData, fieldName)` - Extract file from FormData
61
+ - `getImageDimensions(buffer)` - Get image width/height
62
+ - `transformImage(buffer, config)` - Apply single transformation
63
+ - `processImageTransformations(buffer, filename, transformations, provider, contentType)` - Process all transformations
64
+
65
+ ## Architecture Patterns
66
+
67
+ ### Field Self-Containment
68
+
69
+ File and image fields follow the self-contained field pattern:
70
+
71
+ ```typescript
72
+ export function file(options): FileFieldConfig {
73
+ return {
74
+ type: 'file',
75
+ ...options,
76
+ getZodSchema: () => z.object({ filename: z.string(), url: z.string().url(), ... }).nullable(),
77
+ getPrismaType: () => ({ type: 'Json', modifiers: '?' }),
78
+ getTypeScriptType: () => ({ type: 'import("@opensaas/stack-storage").FileMetadata | null', optional: true }),
79
+ }
80
+ }
81
+ ```
82
+
83
+ No changes to core generators - fields define their own Prisma/TS types.
84
+
85
+ ### Storage Provider Interface
86
+
87
+ All storage backends implement `StorageProvider`:
88
+
89
+ ```typescript
90
+ interface StorageProvider {
91
+ upload(
92
+ file: Buffer | Uint8Array,
93
+ filename: string,
94
+ options?: UploadOptions,
95
+ ): Promise<UploadResult>
96
+ download(filename: string): Promise<Buffer>
97
+ delete(filename: string): Promise<void>
98
+ getUrl(filename: string): string
99
+ getSignedUrl?(filename: string, expiresIn?: number): Promise<string> // Optional
100
+ }
101
+ ```
102
+
103
+ ### Config-Level Storage
104
+
105
+ Storage config is added to `OpenSaasConfig` (similar to auth pattern):
106
+
107
+ ```typescript
108
+ export type OpenSaasConfig = {
109
+ db: DatabaseConfig
110
+ lists: Record<string, ListConfig>
111
+ storage?: StorageConfig // Maps names to provider configs
112
+ // ...
113
+ }
114
+ ```
115
+
116
+ ### Named Storage Providers
117
+
118
+ Multiple storage providers can be configured:
119
+
120
+ ```typescript
121
+ storage: {
122
+ avatars: s3Storage({ bucket: 'avatars', region: 'us-east-1' }),
123
+ documents: localStorage({ uploadDir: './uploads', serveUrl: '/api/files' }),
124
+ videos: vercelBlobStorage({ token: process.env.BLOB_TOKEN }),
125
+ }
126
+ ```
127
+
128
+ Fields reference providers by name:
129
+
130
+ ```typescript
131
+ avatar: image({ storage: 'avatars' }),
132
+ resume: file({ storage: 'documents' }),
133
+ ```
134
+
135
+ ### JSON Metadata Storage
136
+
137
+ Files and images store metadata as JSON (leveraging existing `json` field type):
138
+
139
+ **Prisma schema:**
140
+
141
+ ```prisma
142
+ model User {
143
+ avatar Json? // ImageMetadata
144
+ resume Json? // FileMetadata
145
+ }
146
+ ```
147
+
148
+ **Runtime types:**
149
+
150
+ ```typescript
151
+ user.avatar // ImageMetadata | null
152
+ user.resume // FileMetadata | null
153
+ ```
154
+
155
+ ### Automatic Upload via Field Hooks
156
+
157
+ Files are uploaded automatically during form submission via `resolveInput` hooks. **No custom upload API routes are needed.**
158
+
159
+ **How it works:**
160
+
161
+ 1. User selects file in UI component
162
+ 2. File object stored in form state
163
+ 3. Form submitted with File object
164
+ 4. Field's `resolveInput` hook uploads file server-side
165
+ 5. Returns FileMetadata for database storage
166
+
167
+ **This provides:**
168
+
169
+ 1. **Atomic uploads** - files only saved if form submission succeeds
170
+ 2. **No orphaned files** - failed submissions don't leave files in storage
171
+ 3. **Automatic security** - uploads happen server-side with access control
172
+ 4. **Simpler code** - no custom upload routes needed
173
+
174
+ ### Image Transformation Pipeline
175
+
176
+ 1. Validate file (size, MIME type)
177
+ 2. Upload original image to storage
178
+ 3. For each transformation:
179
+ - Apply transformation with sharp
180
+ - Upload transformed image
181
+ - Return transformation metadata
182
+ 4. Return ImageMetadata with all URLs
183
+
184
+ **Transformations stored with original:**
185
+
186
+ ```typescript
187
+ {
188
+ url: "https://bucket.s3.amazonaws.com/original.jpg",
189
+ transformations: {
190
+ thumbnail: { url: "https://bucket.s3.amazonaws.com/original-thumbnail.jpg", width: 100, height: 100 },
191
+ large: { url: "https://bucket.s3.amazonaws.com/original-large.jpg", width: 1200, height: 1200 }
192
+ }
193
+ }
194
+ ```
195
+
196
+ ### UI Component Integration
197
+
198
+ File/image fields work in admin UI via component registry:
199
+
200
+ ```typescript
201
+ // packages/ui/src/components/fields/registry.ts
202
+ export const fieldComponentRegistry = {
203
+ file: FileField,
204
+ image: ImageField,
205
+ // ...
206
+ }
207
+ ```
208
+
209
+ Components accept `File | FileMetadata | null` as values:
210
+
211
+ - New uploads: File object stored in form state
212
+ - Existing files: FileMetadata from database
213
+ - Deleted files: null
214
+
215
+ ## Integration Points
216
+
217
+ ### With @opensaas/stack-core
218
+
219
+ - `StorageConfig` added to `OpenSaasConfig` type
220
+ - Field builders use `BaseFieldConfig` interface
221
+ - Generators delegate to field methods (no core changes)
222
+
223
+ ### With @opensaas/stack-ui
224
+
225
+ - `FileField` component with drag-and-drop
226
+ - `ImageField` component with preview
227
+ - Registered in field component registry
228
+ - Components require `onUpload` prop (developer implements)
229
+
230
+ ### With @opensaas/stack-storage-s3
231
+
232
+ - S3StorageProvider implements `StorageProvider`
233
+ - Supports AWS S3 and S3-compatible services (MinIO, Backblaze, etc.)
234
+ - Optional signed URLs for private files
235
+
236
+ ### With @opensaas/stack-storage-vercel
237
+
238
+ - VercelBlobStorageProvider implements `StorageProvider`
239
+ - Uses `@vercel/blob` package
240
+ - Optimized for Vercel deployments
241
+
242
+ ## Common Patterns
243
+
244
+ ### Basic Config
245
+
246
+ ```typescript
247
+ import { config, list } from '@opensaas/stack-core'
248
+ import { localStorage } from '@opensaas/stack-storage'
249
+ import { file, image } from '@opensaas/stack-storage/fields'
250
+
251
+ export default config({
252
+ storage: {
253
+ files: localStorage({
254
+ uploadDir: './public/uploads',
255
+ serveUrl: '/uploads',
256
+ }),
257
+ },
258
+ lists: {
259
+ Post: list({
260
+ fields: {
261
+ coverImage: image({
262
+ storage: 'files',
263
+ transformations: {
264
+ thumbnail: { width: 300, height: 200, fit: 'cover' },
265
+ },
266
+ }),
267
+ },
268
+ }),
269
+ },
270
+ })
271
+ ```
272
+
273
+ ### Multiple Storage Providers
274
+
275
+ ```typescript
276
+ storage: {
277
+ avatars: s3Storage({
278
+ bucket: 'user-avatars',
279
+ region: 'us-east-1',
280
+ acl: 'public-read',
281
+ }),
282
+ documents: localStorage({
283
+ uploadDir: './private/documents',
284
+ serveUrl: '/api/files', // Served through auth-protected route
285
+ }),
286
+ }
287
+ ```
288
+
289
+ ### Automatic File Cleanup
290
+
291
+ Enable automatic cleanup of files when records are deleted or files are replaced:
292
+
293
+ ```typescript
294
+ User: list({
295
+ fields: {
296
+ avatar: image({
297
+ storage: 'avatars',
298
+ cleanupOnDelete: true, // Delete avatar when user deleted
299
+ cleanupOnReplace: true, // Delete old avatar when new one uploaded
300
+ transformations: {
301
+ thumbnail: { width: 100, height: 100 },
302
+ },
303
+ }),
304
+ },
305
+ }),
306
+ ```
307
+
308
+ ### Serving Private Files
309
+
310
+ ```typescript
311
+ // app/api/files/[filename]/route.ts
312
+ import { createStorageProvider } from '@opensaas/stack-storage/runtime'
313
+ import config from '@/opensaas.config'
314
+
315
+ export async function GET(request: NextRequest, { params }: { params: { filename: string } }) {
316
+ const session = await getSession()
317
+ if (!session) {
318
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
319
+ }
320
+
321
+ // Get storage provider
322
+ const provider = createStorageProvider(config, 'documents')
323
+
324
+ // Download file
325
+ const buffer = await provider.download(params.filename)
326
+
327
+ // Return file
328
+ return new NextResponse(buffer, {
329
+ headers: {
330
+ 'Content-Type': 'application/octet-stream',
331
+ 'Content-Disposition': `attachment; filename="${params.filename}"`,
332
+ },
333
+ })
334
+ }
335
+ ```
336
+
337
+ ### Image Transformations
338
+
339
+ ```typescript
340
+ avatar: image({
341
+ storage: 'avatars',
342
+ transformations: {
343
+ thumbnail: { width: 100, height: 100, fit: 'cover', format: 'webp', quality: 80 },
344
+ small: { width: 200, height: 200, fit: 'cover', format: 'webp' },
345
+ medium: { width: 400, height: 400, fit: 'cover', format: 'webp' },
346
+ large: { width: 800, height: 800, fit: 'inside', format: 'jpeg', quality: 90 },
347
+ },
348
+ validation: {
349
+ maxFileSize: 10 * 1024 * 1024,
350
+ acceptedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
351
+ },
352
+ })
353
+ ```
354
+
355
+ ### Custom Storage Provider
356
+
357
+ ```typescript
358
+ // lib/cloudflare-r2-storage.ts
359
+ import type { StorageProvider } from '@opensaas/stack-storage'
360
+
361
+ export class CloudflareR2StorageProvider implements StorageProvider {
362
+ async upload(file: Buffer, filename: string, options?) {
363
+ // Upload to Cloudflare R2
364
+ // ...
365
+ return { filename, url, size, contentType }
366
+ }
367
+
368
+ async download(filename: string) {
369
+ // Download from R2
370
+ // ...
371
+ }
372
+
373
+ async delete(filename: string) {
374
+ // Delete from R2
375
+ // ...
376
+ }
377
+
378
+ getUrl(filename: string) {
379
+ return `https://r2.example.com/${filename}`
380
+ }
381
+ }
382
+
383
+ // Register in runtime
384
+ import { createStorageProvider } from '@opensaas/stack-storage/runtime'
385
+
386
+ // Extend createStorageProvider to support 'cloudflare-r2' type
387
+ ```
388
+
389
+ ## Type Safety
390
+
391
+ All types are strongly typed:
392
+
393
+ - `FileMetadata` and `ImageMetadata` for database storage
394
+ - `StorageProvider` interface for custom providers
395
+ - Field configs fully typed with TypeScript
396
+ - Validation options typed with Zod
397
+
398
+ Avoid `any` - all internal utilities use proper types.
399
+
400
+ ## Performance Considerations
401
+
402
+ - **Image transformations** happen during upload (one-time cost)
403
+ - **Sharp** is fast but CPU-intensive (consider background jobs for large images)
404
+ - **Separate provider packages** reduce bundle size (only install what you use)
405
+ - **JSON storage** is efficient for metadata (no additional tables)
406
+ - **CDN integration** via custom domains or CloudFront
407
+
408
+ ## Security
409
+
410
+ - **Developer-controlled routes** allow custom auth/validation
411
+ - **MIME type validation** prevents file type spoofing
412
+ - **File size limits** prevent DoS attacks
413
+ - **Access control** enforced in upload routes
414
+ - **Signed URLs** for private S3 files (optional)
415
+ - **No direct file access** unless served through developer routes
416
+
417
+ ## Future Enhancements
418
+
419
+ Potential additions:
420
+
421
+ - Background job support for large image processing
422
+ - Video/audio field types
423
+ - CDN invalidation hooks
424
+ - Image optimization (compression, format conversion)
425
+ - Cloud provider integrations (Azure Blob, Google Cloud Storage)
426
+ - File virus scanning integration
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 OpenSaas Stack Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.