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,290 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { StorageProvider } from "./StorageProvider";
4
+ import type { UploadConfiguration, StorageResult, FileMetadata } from "../../types/upload.types";
5
+ import { logger as MainLogger } from "../Logger";
6
+
7
+ const logger = MainLogger.child({ scope: "LocalStorageProvider" });
8
+
9
+ /**
10
+ * Local File System Storage Provider
11
+ * Handles file storage on the local filesystem
12
+ */
13
+ export class LocalStorageProvider extends StorageProvider {
14
+ private basePath: string;
15
+ private baseUrl: string;
16
+
17
+ constructor(config: {
18
+ basePath?: string;
19
+ baseUrl?: string;
20
+ } = {}) {
21
+ super("local", config);
22
+ this.basePath = config.basePath || "./public";
23
+ this.baseUrl = config.baseUrl || "";
24
+ this.validateConfig();
25
+ }
26
+
27
+ public async initialize(): Promise<void> {
28
+ logger.info("Initializing Local Storage Provider");
29
+
30
+ // Ensure base directory exists
31
+ if (!fs.existsSync(this.basePath)) {
32
+ fs.mkdirSync(this.basePath, { recursive: true });
33
+ logger.info(`Created base directory: ${this.basePath}`);
34
+ }
35
+ }
36
+
37
+ public async store(
38
+ file: File,
39
+ metadata: FileMetadata,
40
+ config: UploadConfiguration
41
+ ): Promise<StorageResult> {
42
+ const uploadDir = path.join(this.basePath, config.uploadPath);
43
+ const fullPath = path.join(uploadDir, metadata.fileName);
44
+ const relativePath = this.buildPath(config.uploadPath, metadata.fileName);
45
+
46
+ logger.info(`Storing file: ${metadata.fileName} to ${fullPath}`);
47
+
48
+ try {
49
+ // Ensure upload directory exists
50
+ if (!fs.existsSync(uploadDir)) {
51
+ fs.mkdirSync(uploadDir, { recursive: true });
52
+ }
53
+
54
+ // Write file to disk
55
+ const buffer = Buffer.from(await file.arrayBuffer());
56
+ fs.writeFileSync(fullPath, buffer);
57
+
58
+ // Generate URL
59
+ const url = this.buildUrl(relativePath);
60
+
61
+ logger.info(`File stored successfully: ${metadata.fileName}`);
62
+
63
+ return {
64
+ path: relativePath,
65
+ url,
66
+ metadata: {
67
+ ...metadata,
68
+ fullPath,
69
+ storedAt: new Date().toISOString()
70
+ }
71
+ };
72
+
73
+ } catch (error) {
74
+ logger.error(`Failed to store file ${metadata.fileName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
75
+ throw new Error(`Failed to store file: ${error instanceof Error ? error.message : 'Unknown error'}`);
76
+ }
77
+ }
78
+
79
+ public async delete(filePath: string): Promise<boolean> {
80
+ const fullPath = path.join(this.basePath, this.sanitizePath(filePath));
81
+
82
+ try {
83
+ if (fs.existsSync(fullPath)) {
84
+ fs.unlinkSync(fullPath);
85
+ logger.info(`File deleted: ${filePath}`);
86
+ return true;
87
+ } else {
88
+ logger.warn(`File not found for deletion: ${filePath}`);
89
+ return false;
90
+ }
91
+ } catch (error) {
92
+ logger.error(`Failed to delete file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
93
+ return false;
94
+ }
95
+ }
96
+
97
+ public async getUrl(filePath: string): Promise<string> {
98
+ return this.buildUrl(filePath);
99
+ }
100
+
101
+ public async exists(filePath: string): Promise<boolean> {
102
+ const fullPath = path.join(this.basePath, this.sanitizePath(filePath));
103
+ return fs.existsSync(fullPath);
104
+ }
105
+
106
+ public async getMetadata(filePath: string): Promise<FileMetadata | null> {
107
+ const fullPath = path.join(this.basePath, this.sanitizePath(filePath));
108
+
109
+ try {
110
+ if (!fs.existsSync(fullPath)) {
111
+ return null;
112
+ }
113
+
114
+ const stats = fs.statSync(fullPath);
115
+ const fileName = path.basename(filePath);
116
+
117
+ return {
118
+ uploadId: "", // Would need to be stored separately
119
+ fileName,
120
+ originalFileName: fileName,
121
+ mimeType: this.getMimeTypeFromExtension(path.extname(fileName)),
122
+ size: stats.size,
123
+ extension: path.extname(fileName),
124
+ uploadedAt: stats.birthtime.toISOString()
125
+ };
126
+
127
+ } catch (error) {
128
+ logger.error(`Failed to get metadata for ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
129
+ return null;
130
+ }
131
+ }
132
+
133
+ public async list(directoryPath: string): Promise<string[]> {
134
+ const fullPath = path.join(this.basePath, this.sanitizePath(directoryPath));
135
+
136
+ try {
137
+ if (!fs.existsSync(fullPath)) {
138
+ return [];
139
+ }
140
+
141
+ const items = fs.readdirSync(fullPath);
142
+ return items.filter(item => {
143
+ const itemPath = path.join(fullPath, item);
144
+ return fs.statSync(itemPath).isFile();
145
+ });
146
+
147
+ } catch (error) {
148
+ logger.error(`Failed to list directory ${directoryPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
149
+ return [];
150
+ }
151
+ }
152
+
153
+ public async getStream(filePath: string): Promise<ReadableStream> {
154
+ const fullPath = path.join(this.basePath, this.sanitizePath(filePath));
155
+
156
+ if (!fs.existsSync(fullPath)) {
157
+ throw new Error(`File not found: ${filePath}`);
158
+ }
159
+
160
+ const fileStream = fs.createReadStream(fullPath);
161
+
162
+ return new ReadableStream({
163
+ start(controller) {
164
+ fileStream.on('data', (chunk) => {
165
+ controller.enqueue(chunk);
166
+ });
167
+
168
+ fileStream.on('end', () => {
169
+ controller.close();
170
+ });
171
+
172
+ fileStream.on('error', (error) => {
173
+ controller.error(error);
174
+ });
175
+ }
176
+ });
177
+ }
178
+
179
+ public async copy(sourcePath: string, destinationPath: string): Promise<boolean> {
180
+ const sourceFullPath = path.join(this.basePath, this.sanitizePath(sourcePath));
181
+ const destFullPath = path.join(this.basePath, this.sanitizePath(destinationPath));
182
+
183
+ try {
184
+ if (!fs.existsSync(sourceFullPath)) {
185
+ logger.warn(`Source file not found: ${sourcePath}`);
186
+ return false;
187
+ }
188
+
189
+ // Ensure destination directory exists
190
+ const destDir = path.dirname(destFullPath);
191
+ if (!fs.existsSync(destDir)) {
192
+ fs.mkdirSync(destDir, { recursive: true });
193
+ }
194
+
195
+ fs.copyFileSync(sourceFullPath, destFullPath);
196
+ logger.info(`File copied from ${sourcePath} to ${destinationPath}`);
197
+ return true;
198
+
199
+ } catch (error) {
200
+ logger.error(`Failed to copy file from ${sourcePath} to ${destinationPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
201
+ return false;
202
+ }
203
+ }
204
+
205
+ public async move(sourcePath: string, destinationPath: string): Promise<boolean> {
206
+ const success = await this.copy(sourcePath, destinationPath);
207
+ if (success) {
208
+ return await this.delete(sourcePath);
209
+ }
210
+ return false;
211
+ }
212
+
213
+ public async getStats(): Promise<{
214
+ totalFiles: number;
215
+ totalSize: number;
216
+ availableSpace?: number;
217
+ }> {
218
+ try {
219
+ const stats = await this.calculateDirectoryStats(this.basePath);
220
+ return stats;
221
+ } catch (error) {
222
+ logger.error(`Failed to get storage stats: ${error instanceof Error ? error.message : 'Unknown error'}`);
223
+ return { totalFiles: 0, totalSize: 0 };
224
+ }
225
+ }
226
+
227
+ public async cleanup(): Promise<void> {
228
+ logger.info("Local storage cleanup - no action needed");
229
+ // For local storage, cleanup might involve removing temp files
230
+ // This is a placeholder for future implementation
231
+ }
232
+
233
+ protected validateConfig(): boolean {
234
+ if (!this.basePath) {
235
+ throw new Error("LocalStorageProvider: basePath is required");
236
+ }
237
+ return true;
238
+ }
239
+
240
+ private buildUrl(filePath: string): string {
241
+ const cleanPath = filePath.startsWith('/') ? filePath : `/${filePath}`;
242
+ return `${this.baseUrl}${cleanPath}`;
243
+ }
244
+
245
+ private getMimeTypeFromExtension(extension: string): string {
246
+ const mimeTypes: Record<string, string> = {
247
+ '.jpg': 'image/jpeg',
248
+ '.jpeg': 'image/jpeg',
249
+ '.png': 'image/png',
250
+ '.gif': 'image/gif',
251
+ '.webp': 'image/webp',
252
+ '.pdf': 'application/pdf',
253
+ '.txt': 'text/plain',
254
+ '.doc': 'application/msword',
255
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
256
+ };
257
+
258
+ return mimeTypes[extension.toLowerCase()] || 'application/octet-stream';
259
+ }
260
+
261
+ private async calculateDirectoryStats(dirPath: string): Promise<{
262
+ totalFiles: number;
263
+ totalSize: number;
264
+ }> {
265
+ let totalFiles = 0;
266
+ let totalSize = 0;
267
+
268
+ const traverse = (currentPath: string): void => {
269
+ if (!fs.existsSync(currentPath)) return;
270
+
271
+ const items = fs.readdirSync(currentPath);
272
+
273
+ for (const item of items) {
274
+ const itemPath = path.join(currentPath, item);
275
+ const stats = fs.statSync(itemPath);
276
+
277
+ if (stats.isFile()) {
278
+ totalFiles++;
279
+ totalSize += stats.size;
280
+ } else if (stats.isDirectory()) {
281
+ traverse(itemPath);
282
+ }
283
+ }
284
+ };
285
+
286
+ traverse(dirPath);
287
+
288
+ return { totalFiles, totalSize };
289
+ }
290
+ }
@@ -0,0 +1,112 @@
1
+ import type { UploadConfiguration, StorageResult, FileMetadata } from "../../types/upload.types";
2
+
3
+ /**
4
+ * Abstract Storage Provider Interface
5
+ * Defines the contract for all storage backend implementations
6
+ */
7
+ export abstract class StorageProvider {
8
+ protected name: string;
9
+ protected config: Record<string, any>;
10
+
11
+ constructor(name: string, config: Record<string, any> = {}) {
12
+ this.name = name;
13
+ this.config = config;
14
+ }
15
+
16
+ /**
17
+ * Get the storage provider name
18
+ */
19
+ public getName(): string {
20
+ return this.name;
21
+ }
22
+
23
+ /**
24
+ * Initialize the storage provider
25
+ */
26
+ public abstract initialize(): Promise<void>;
27
+
28
+ /**
29
+ * Store a file
30
+ */
31
+ public abstract store(
32
+ file: File,
33
+ metadata: FileMetadata,
34
+ config: UploadConfiguration
35
+ ): Promise<StorageResult>;
36
+
37
+ /**
38
+ * Delete a file
39
+ */
40
+ public abstract delete(path: string): Promise<boolean>;
41
+
42
+ /**
43
+ * Get file URL
44
+ */
45
+ public abstract getUrl(path: string): Promise<string>;
46
+
47
+ /**
48
+ * Check if file exists
49
+ */
50
+ public abstract exists(path: string): Promise<boolean>;
51
+
52
+ /**
53
+ * Get file metadata
54
+ */
55
+ public abstract getMetadata(path: string): Promise<FileMetadata | null>;
56
+
57
+ /**
58
+ * List files in directory
59
+ */
60
+ public abstract list(path: string): Promise<string[]>;
61
+
62
+ /**
63
+ * Get file stream
64
+ */
65
+ public abstract getStream(path: string): Promise<ReadableStream>;
66
+
67
+ /**
68
+ * Copy file
69
+ */
70
+ public abstract copy(sourcePath: string, destinationPath: string): Promise<boolean>;
71
+
72
+ /**
73
+ * Move file
74
+ */
75
+ public abstract move(sourcePath: string, destinationPath: string): Promise<boolean>;
76
+
77
+ /**
78
+ * Get storage statistics
79
+ */
80
+ public abstract getStats(): Promise<{
81
+ totalFiles: number;
82
+ totalSize: number;
83
+ availableSpace?: number;
84
+ }>;
85
+
86
+ /**
87
+ * Cleanup temporary files
88
+ */
89
+ public abstract cleanup(): Promise<void>;
90
+
91
+ /**
92
+ * Validate storage provider configuration
93
+ */
94
+ protected abstract validateConfig(): boolean;
95
+
96
+ /**
97
+ * Build full file path
98
+ */
99
+ protected buildPath(uploadPath: string, fileName: string): string {
100
+ return `${uploadPath}/${fileName}`.replace(/\/+/g, '/');
101
+ }
102
+
103
+ /**
104
+ * Sanitize path to prevent directory traversal
105
+ */
106
+ protected sanitizePath(path: string): string {
107
+ return path
108
+ .replace(/\.\./g, '')
109
+ .replace(/\/+/g, '/')
110
+ .replace(/^\/+/, '');
111
+ }
112
+ }