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.
- package/.github/workflows/deploy-docs.yml +57 -0
- package/LICENSE.md +1 -1
- package/README.md +2 -28
- package/TODO.md +8 -1
- package/bun.lock +3 -0
- package/config/upload.config.ts +135 -0
- package/core/App.ts +168 -4
- package/core/ArcheType.ts +122 -0
- package/core/BatchLoader.ts +100 -0
- package/core/ComponentRegistry.ts +4 -3
- package/core/Components.ts +2 -2
- package/core/Decorators.ts +15 -8
- package/core/Entity.ts +193 -14
- package/core/EntityCache.ts +15 -0
- package/core/EntityHookManager.ts +855 -0
- package/core/EntityManager.ts +12 -2
- package/core/ErrorHandler.ts +64 -7
- package/core/FileValidator.ts +284 -0
- package/core/Query.ts +503 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +89 -0
- package/core/SchedulerManager.ts +710 -0
- package/core/UploadManager.ts +261 -0
- package/core/components/UploadComponent.ts +206 -0
- package/core/decorators/EntityHooks.ts +190 -0
- package/core/decorators/ScheduledTask.ts +83 -0
- package/core/events/EntityLifecycleEvents.ts +177 -0
- package/core/processors/ImageProcessor.ts +423 -0
- package/core/storage/LocalStorageProvider.ts +290 -0
- package/core/storage/StorageProvider.ts +112 -0
- package/database/DatabaseHelper.ts +183 -58
- package/database/index.ts +5 -5
- package/database/sqlHelpers.ts +7 -0
- package/docs/README.md +149 -0
- package/docs/_coverpage.md +36 -0
- package/docs/_sidebar.md +23 -0
- package/docs/api/core.md +568 -0
- package/docs/api/hooks.md +554 -0
- package/docs/api/index.md +222 -0
- package/docs/api/query.md +678 -0
- package/docs/api/service.md +744 -0
- package/docs/core-concepts/archetypes.md +512 -0
- package/docs/core-concepts/components.md +498 -0
- package/docs/core-concepts/entity.md +314 -0
- package/docs/core-concepts/hooks.md +683 -0
- package/docs/core-concepts/query.md +588 -0
- package/docs/core-concepts/services.md +647 -0
- package/docs/examples/code-examples.md +425 -0
- package/docs/getting-started.md +337 -0
- package/docs/index.html +97 -0
- package/gql/Generator.ts +58 -35
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +65 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +19 -10
- package/rest/Generator.ts +3 -0
- package/rest/index.ts +22 -0
- package/service/Service.ts +1 -1
- package/service/ServiceRegistry.ts +10 -6
- package/service/index.ts +12 -1
- package/tests/bench/insert.bench.ts +59 -0
- package/tests/bench/relations.bench.ts +269 -0
- package/tests/bench/sorting.bench.ts +415 -0
- package/tests/component-hooks.test.ts +1409 -0
- package/tests/component.test.ts +338 -0
- package/tests/errorHandling.test.ts +155 -0
- package/tests/hooks.test.ts +666 -0
- package/tests/query-sorting.test.ts +101 -0
- package/tests/relations.test.ts +169 -0
- package/tests/scheduler.test.ts +724 -0
- package/tsconfig.json +35 -34
- package/types/graphql.types.ts +87 -0
- package/types/hooks.types.ts +141 -0
- package/types/scheduler.types.ts +165 -0
- package/types/upload.types.ts +184 -0
- package/upload/index.ts +140 -0
- package/utils/UploadHelper.ts +305 -0
- package/utils/cronParser.ts +366 -0
- package/utils/errorMessages.ts +151 -0
- 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
|
+
}
|