bunsane 0.1.0 → 0.1.2

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 (82) hide show
  1. package/.github/workflows/deploy-docs.yml +57 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -28
  4. package/TODO.md +8 -1
  5. package/bun.lock +3 -0
  6. package/config/upload.config.ts +135 -0
  7. package/core/App.ts +168 -4
  8. package/core/ArcheType.ts +122 -0
  9. package/core/BatchLoader.ts +100 -0
  10. package/core/ComponentRegistry.ts +4 -3
  11. package/core/Components.ts +2 -2
  12. package/core/Decorators.ts +15 -8
  13. package/core/Entity.ts +193 -14
  14. package/core/EntityCache.ts +15 -0
  15. package/core/EntityHookManager.ts +855 -0
  16. package/core/EntityManager.ts +12 -2
  17. package/core/ErrorHandler.ts +64 -7
  18. package/core/FileValidator.ts +284 -0
  19. package/core/Query.ts +503 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +89 -0
  22. package/core/SchedulerManager.ts +710 -0
  23. package/core/UploadManager.ts +261 -0
  24. package/core/components/UploadComponent.ts +206 -0
  25. package/core/decorators/EntityHooks.ts +190 -0
  26. package/core/decorators/ScheduledTask.ts +83 -0
  27. package/core/events/EntityLifecycleEvents.ts +177 -0
  28. package/core/processors/ImageProcessor.ts +423 -0
  29. package/core/storage/LocalStorageProvider.ts +290 -0
  30. package/core/storage/StorageProvider.ts +112 -0
  31. package/database/DatabaseHelper.ts +183 -58
  32. package/database/index.ts +5 -5
  33. package/database/sqlHelpers.ts +7 -0
  34. package/docs/README.md +149 -0
  35. package/docs/_coverpage.md +36 -0
  36. package/docs/_sidebar.md +23 -0
  37. package/docs/api/core.md +568 -0
  38. package/docs/api/hooks.md +554 -0
  39. package/docs/api/index.md +222 -0
  40. package/docs/api/query.md +678 -0
  41. package/docs/api/service.md +744 -0
  42. package/docs/core-concepts/archetypes.md +512 -0
  43. package/docs/core-concepts/components.md +498 -0
  44. package/docs/core-concepts/entity.md +314 -0
  45. package/docs/core-concepts/hooks.md +683 -0
  46. package/docs/core-concepts/query.md +588 -0
  47. package/docs/core-concepts/services.md +647 -0
  48. package/docs/examples/code-examples.md +425 -0
  49. package/docs/getting-started.md +337 -0
  50. package/docs/index.html +97 -0
  51. package/gql/Generator.ts +58 -35
  52. package/gql/decorators/Upload.ts +176 -0
  53. package/gql/helpers.ts +67 -0
  54. package/gql/index.ts +65 -31
  55. package/gql/types.ts +1 -1
  56. package/index.ts +79 -11
  57. package/package.json +19 -10
  58. package/rest/Generator.ts +3 -0
  59. package/rest/index.ts +22 -0
  60. package/service/Service.ts +1 -1
  61. package/service/ServiceRegistry.ts +10 -6
  62. package/service/index.ts +12 -1
  63. package/tests/bench/insert.bench.ts +59 -0
  64. package/tests/bench/relations.bench.ts +269 -0
  65. package/tests/bench/sorting.bench.ts +415 -0
  66. package/tests/component-hooks.test.ts +1409 -0
  67. package/tests/component.test.ts +338 -0
  68. package/tests/errorHandling.test.ts +155 -0
  69. package/tests/hooks.test.ts +666 -0
  70. package/tests/query-sorting.test.ts +101 -0
  71. package/tests/relations.test.ts +169 -0
  72. package/tests/scheduler.test.ts +724 -0
  73. package/tsconfig.json +35 -34
  74. package/types/graphql.types.ts +87 -0
  75. package/types/hooks.types.ts +141 -0
  76. package/types/scheduler.types.ts +165 -0
  77. package/types/upload.types.ts +184 -0
  78. package/upload/index.ts +140 -0
  79. package/utils/UploadHelper.ts +305 -0
  80. package/utils/cronParser.ts +366 -0
  81. package/utils/errorMessages.ts +151 -0
  82. package/core/Events.ts +0 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Bunsane Upload System
3
+ * Comprehensive file upload handling for the Bunsane framework
4
+ */
5
+
6
+ // Core Upload System
7
+ export { UploadManager } from "../core/UploadManager";
8
+ export { FileValidator } from "../core/FileValidator";
9
+
10
+ // Storage Providers
11
+ export { StorageProvider } from "../core/storage/StorageProvider";
12
+ export { LocalStorageProvider } from "../core/storage/LocalStorageProvider";
13
+
14
+ // Components
15
+ export { UploadComponent, ImageMetadataComponent } from "../core/components/UploadComponent";
16
+
17
+ // Processors
18
+ export { ImageProcessor } from "../core/processors/ImageProcessor";
19
+
20
+ // Utilities
21
+ export { UploadHelper } from "../utils/UploadHelper";
22
+
23
+ // GraphQL Decorators
24
+ export {
25
+ Upload,
26
+ UploadField,
27
+ BatchUpload,
28
+ RequiredUpload,
29
+ UploadDecorators,
30
+ getUploadConfiguration
31
+ } from "../gql/decorators/Upload";
32
+
33
+ // Configuration
34
+ export {
35
+ DEFAULT_UPLOAD_CONFIG,
36
+ IMAGE_UPLOAD_CONFIG,
37
+ DOCUMENT_UPLOAD_CONFIG,
38
+ AVATAR_UPLOAD_CONFIG,
39
+ SECURE_UPLOAD_CONFIG
40
+ } from "../config/upload.config";
41
+
42
+ // Types
43
+ export type {
44
+ UploadConfiguration,
45
+ ImageProcessingOptions,
46
+ ValidationOptions,
47
+ ValidationResult,
48
+ UploadResult,
49
+ UploadError,
50
+ UploadErrorCode,
51
+ FileMetadata,
52
+ StorageResult,
53
+ UploadProgress,
54
+ BatchUploadResult,
55
+ UploadComponentData,
56
+ UploadDecoratorConfig,
57
+ UploadGraphQLType
58
+ } from "../types/upload.types";
59
+
60
+ // Imports for internal use
61
+ import { UploadManager } from "../core/UploadManager";
62
+ import type { UploadConfiguration } from "../types/upload.types";
63
+
64
+ /**
65
+ * Initialize the upload system with default configuration
66
+ */
67
+ export async function initializeUploadSystem(config?: Partial<UploadConfiguration>): Promise<void> {
68
+ const uploadManager = UploadManager.getInstance();
69
+
70
+ if (config) {
71
+ uploadManager.updateConfiguration(config);
72
+ }
73
+
74
+ // Initialize default storage provider
75
+ await uploadManager.getStorageProvider("local").initialize();
76
+ }
77
+
78
+ /**
79
+ * Quick setup functions for common use cases
80
+ */
81
+ export class QuickSetup {
82
+ /**
83
+ * Setup for image uploads with thumbnails
84
+ */
85
+ static async forImages(): Promise<void> {
86
+ const uploadManager = UploadManager.getInstance();
87
+ uploadManager.updateConfiguration({
88
+ maxFileSize: 5 * 1024 * 1024, // 5MB
89
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
90
+ allowedExtensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
91
+ generateThumbnails: true,
92
+ imageProcessing: {
93
+ generateThumbnails: true,
94
+ thumbnailSizes: [
95
+ { width: 150, height: 150, suffix: "_thumb" },
96
+ { width: 300, height: 300, suffix: "_medium" }
97
+ ],
98
+ compress: true,
99
+ quality: 85
100
+ }
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Setup for document uploads
106
+ */
107
+ static async forDocuments(): Promise<void> {
108
+ const uploadManager = UploadManager.getInstance();
109
+ uploadManager.updateConfiguration({
110
+ maxFileSize: 25 * 1024 * 1024, // 25MB
111
+ allowedMimeTypes: [
112
+ "application/pdf",
113
+ "text/plain",
114
+ "application/msword",
115
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
116
+ ],
117
+ allowedExtensions: [".pdf", ".txt", ".doc", ".docx"],
118
+ validateFileSignature: true,
119
+ generateThumbnails: false
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Setup for secure uploads with strict validation
125
+ */
126
+ static async forSecureUploads(): Promise<void> {
127
+ const uploadManager = UploadManager.getInstance();
128
+ uploadManager.updateConfiguration({
129
+ maxFileSize: 1 * 1024 * 1024, // 1MB
130
+ allowedMimeTypes: ["image/jpeg", "image/png"],
131
+ allowedExtensions: [".jpg", ".jpeg", ".png"],
132
+ validateFileSignature: true,
133
+ sanitizeFileName: true,
134
+ validation: {
135
+ scanForMalware: true,
136
+ strictMimeType: true
137
+ }
138
+ });
139
+ }
140
+ }
@@ -0,0 +1,305 @@
1
+ import { UploadManager } from "../core/UploadManager";
2
+ import { UploadComponent, ImageMetadataComponent } from "../core/components/UploadComponent";
3
+ import { Entity } from "../core/Entity";
4
+ import type { UploadConfiguration, UploadResult, BatchUploadResult } from "../types/upload.types";
5
+ import { logger as MainLogger } from "../core/Logger";
6
+
7
+ const logger = MainLogger.child({ scope: "UploadHelper" });
8
+
9
+ /**
10
+ * UploadHelper - Utility class for common upload operations
11
+ * Provides convenience methods for handling uploads in services
12
+ */
13
+ export class UploadHelper {
14
+ private static uploadManager = UploadManager.getInstance();
15
+
16
+ /**
17
+ * Process single file upload and attach to entity
18
+ */
19
+ static async processUploadForEntity(
20
+ entity: Entity,
21
+ file: File,
22
+ config?: Partial<UploadConfiguration>
23
+ ): Promise<UploadResult> {
24
+ try {
25
+ logger.info(`Processing upload for entity ${entity.id}`);
26
+
27
+ const result = await this.uploadManager.uploadFile(file, config);
28
+
29
+ if (result.success && result.uploadId) {
30
+ // Create and attach upload component
31
+ const uploadComponent = new UploadComponent();
32
+ uploadComponent.setUploadData({
33
+ uploadId: result.uploadId,
34
+ fileName: result.fileName!,
35
+ originalFileName: result.originalFileName!,
36
+ mimeType: result.mimeType!,
37
+ size: result.size!,
38
+ path: result.path!,
39
+ url: result.url!,
40
+ uploadedAt: new Date().toISOString(),
41
+ metadata: result.metadata || {}
42
+ });
43
+
44
+ entity.add(UploadComponent, uploadComponent.data());
45
+
46
+ // Add image metadata if it's an image
47
+ if (result.mimeType?.startsWith('image/')) {
48
+ await this.addImageMetadata(entity, file, result);
49
+ }
50
+
51
+ logger.info(`Upload component attached to entity ${entity.id}`);
52
+ }
53
+
54
+ return result;
55
+
56
+ } catch (error) {
57
+ logger.error(`Failed to process upload for entity ${entity.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Process multiple files for an entity
64
+ */
65
+ static async processBatchUploadForEntity(
66
+ entity: Entity,
67
+ files: File[],
68
+ config?: Partial<UploadConfiguration>
69
+ ): Promise<BatchUploadResult> {
70
+ logger.info(`Processing batch upload of ${files.length} files for entity ${entity.id}`);
71
+
72
+ const results = await this.uploadManager.uploadFiles(files, config);
73
+
74
+ let successfulUploads = 0;
75
+ let failedUploads = 0;
76
+ const errors: any[] = [];
77
+
78
+ for (const result of results) {
79
+ if (result.success) {
80
+ successfulUploads++;
81
+
82
+ // Attach upload component to entity
83
+ const uploadComponent = new UploadComponent();
84
+ uploadComponent.setUploadData({
85
+ uploadId: result.uploadId!,
86
+ fileName: result.fileName!,
87
+ originalFileName: result.originalFileName!,
88
+ mimeType: result.mimeType!,
89
+ size: result.size!,
90
+ path: result.path!,
91
+ url: result.url!,
92
+ uploadedAt: new Date().toISOString(),
93
+ metadata: result.metadata || {}
94
+ });
95
+
96
+ entity.add(UploadComponent, uploadComponent.data());
97
+
98
+ } else {
99
+ failedUploads++;
100
+ if (result.error) {
101
+ errors.push(result.error);
102
+ }
103
+ }
104
+ }
105
+
106
+ return {
107
+ totalFiles: files.length,
108
+ successfulUploads,
109
+ failedUploads,
110
+ results,
111
+ errors
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Replace existing upload on entity
117
+ */
118
+ static async replaceUploadForEntity(
119
+ entity: Entity,
120
+ file: File,
121
+ config?: Partial<UploadConfiguration>
122
+ ): Promise<UploadResult> {
123
+ // Remove existing upload component if present
124
+ const existingUpload = await entity.get(UploadComponent);
125
+ if (existingUpload) {
126
+ // Delete old file
127
+ await this.uploadManager.deleteFile(existingUpload.path);
128
+ // Remove component data will be handled by the new upload
129
+ }
130
+
131
+ return await this.processUploadForEntity(entity, file, config);
132
+ }
133
+
134
+ /**
135
+ * Get upload URLs for entity
136
+ */
137
+ static async getUploadUrlsForEntity(entity: Entity): Promise<string[]> {
138
+ const upload = await entity.get(UploadComponent);
139
+ return upload ? [upload.url] : [];
140
+ }
141
+
142
+ /**
143
+ * Clean up orphaned uploads for entity
144
+ */
145
+ static async cleanupOrphanedUploads(entity: Entity): Promise<number> {
146
+ let cleaned = 0;
147
+
148
+ try {
149
+ const upload = await entity.get(UploadComponent);
150
+
151
+ if (upload) {
152
+ const exists = await this.uploadManager.getStorageProvider().exists(upload.path);
153
+ if (!exists) {
154
+ // File doesn't exist, remove component
155
+ // This would require entity method to remove specific component instance
156
+ cleaned++;
157
+ logger.info(`Cleaned orphaned upload reference: ${upload.path}`);
158
+ }
159
+ }
160
+
161
+ } catch (error) {
162
+ logger.error(`Failed to cleanup orphaned uploads for entity ${entity.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
163
+ }
164
+
165
+ return cleaned;
166
+ }
167
+
168
+ /**
169
+ * Get total storage used by entity
170
+ */
171
+ static async getEntityStorageUsage(entity: Entity): Promise<number> {
172
+ const upload = await entity.get(UploadComponent);
173
+ return upload ? upload.size : 0;
174
+ }
175
+
176
+ /**
177
+ * Validate file before upload
178
+ */
179
+ static async validateFile(file: File, config: UploadConfiguration): Promise<boolean> {
180
+ const validator = (this.uploadManager as any).fileValidator;
181
+ const result = await validator.validate(file, config);
182
+ return result.valid;
183
+ }
184
+
185
+ /**
186
+ * Generate secure file URL with expiration
187
+ */
188
+ static async getSecureFileUrl(
189
+ path: string,
190
+ expiresIn: number = 3600, // 1 hour default
191
+ storageProvider?: string
192
+ ): Promise<string | null> {
193
+ // This would be implemented by storage providers that support signed URLs
194
+ const provider = this.uploadManager.getStorageProvider(storageProvider);
195
+
196
+ // For now, return regular URL
197
+ // TODO: Implement signed URL generation for cloud providers
198
+ return await provider.getUrl(path);
199
+ }
200
+
201
+ /**
202
+ * Copy upload from one entity to another
203
+ */
204
+ static async copyUploadBetweenEntities(
205
+ sourceEntity: Entity,
206
+ targetEntity: Entity,
207
+ preserveOriginal: boolean = true
208
+ ): Promise<boolean> {
209
+ try {
210
+ const sourceUpload = await sourceEntity.get(UploadComponent);
211
+ if (!sourceUpload) {
212
+ return false;
213
+ }
214
+
215
+ const provider = this.uploadManager.getStorageProvider();
216
+
217
+ if (preserveOriginal) {
218
+ // Copy file to new location
219
+ const newPath = sourceUpload.path.replace(
220
+ sourceEntity.id,
221
+ targetEntity.id
222
+ );
223
+
224
+ const success = await provider.copy(sourceUpload.path, newPath);
225
+ if (success) {
226
+ // Create new upload component for target entity
227
+ const newUpload = new UploadComponent();
228
+ newUpload.setUploadData({
229
+ uploadId: targetEntity.id,
230
+ fileName: sourceUpload.fileName,
231
+ originalFileName: sourceUpload.originalFileName,
232
+ mimeType: sourceUpload.mimeType,
233
+ size: sourceUpload.size,
234
+ path: newPath,
235
+ url: await provider.getUrl(newPath),
236
+ uploadedAt: sourceUpload.uploadedAt,
237
+ metadata: JSON.parse(sourceUpload.metadata || '{}')
238
+ });
239
+
240
+ targetEntity.add(UploadComponent, newUpload.data());
241
+ return true;
242
+ }
243
+ } else {
244
+ // Move file
245
+ const newPath = sourceUpload.path.replace(
246
+ sourceEntity.id,
247
+ targetEntity.id
248
+ );
249
+
250
+ const success = await provider.move(sourceUpload.path, newPath);
251
+ if (success) {
252
+ // Remove from source and add to target
253
+ const newUpload = new UploadComponent();
254
+ newUpload.setUploadData({
255
+ uploadId: targetEntity.id,
256
+ fileName: sourceUpload.fileName,
257
+ originalFileName: sourceUpload.originalFileName,
258
+ mimeType: sourceUpload.mimeType,
259
+ size: sourceUpload.size,
260
+ path: newPath,
261
+ url: await provider.getUrl(newPath),
262
+ uploadedAt: sourceUpload.uploadedAt,
263
+ metadata: JSON.parse(sourceUpload.metadata || '{}')
264
+ });
265
+
266
+ targetEntity.add(UploadComponent, newUpload.data());
267
+ // TODO: Remove from source entity
268
+ return true;
269
+ }
270
+ }
271
+
272
+ return false;
273
+
274
+ } catch (error) {
275
+ logger.error(`Failed to copy upload between entities: ${error instanceof Error ? error.message : 'Unknown error'}`);
276
+ return false;
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Add image metadata to entity
282
+ */
283
+ private static async addImageMetadata(
284
+ entity: Entity,
285
+ file: File,
286
+ uploadResult: UploadResult
287
+ ): Promise<void> {
288
+ try {
289
+ // For now, we'll add basic metadata
290
+ // In a full implementation, this would use an image processing library
291
+ const imageMetadata = new ImageMetadataComponent();
292
+
293
+ // Set basic metadata (would be extracted from actual image)
294
+ imageMetadata.width = 0; // Would be set from image analysis
295
+ imageMetadata.height = 0; // Would be set from image analysis
296
+ imageMetadata.hasAlpha = file.type === 'image/png';
297
+ imageMetadata.isAnimated = file.type === 'image/gif';
298
+
299
+ entity.add(ImageMetadataComponent, imageMetadata.data());
300
+
301
+ } catch (error) {
302
+ logger.warn(`Failed to add image metadata: ${error instanceof Error ? error.message : 'Unknown error'}`);
303
+ }
304
+ }
305
+ }