bunsane 0.1.0 → 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.
- 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 +119 -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 +159 -12
- 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 +453 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +65 -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 +1 -1
- 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/examples/hooks/README.md +228 -0
- package/examples/hooks/audit-logger.ts +495 -0
- package/gql/Generator.ts +56 -34
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +55 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +5 -4
- 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 +205 -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/validate-docs.sh +90 -0
- package/core/Events.ts +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { logger as MainLogger } from "./Logger";
|
|
2
|
+
import { uuidv7 } from "../utils/uuid";
|
|
3
|
+
import type { StorageProvider } from "./storage/StorageProvider";
|
|
4
|
+
import { LocalStorageProvider } from "./storage/LocalStorageProvider";
|
|
5
|
+
import type { UploadConfiguration, UploadResult, UploadError, FileMetadata } from "../types/upload.types";
|
|
6
|
+
import { FileValidator } from "./FileValidator";
|
|
7
|
+
|
|
8
|
+
const logger = MainLogger.child({ scope: "UploadManager" });
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* UploadManager - Singleton class for managing file uploads
|
|
12
|
+
* Provides centralized upload handling with pluggable storage backends
|
|
13
|
+
*/
|
|
14
|
+
export class UploadManager {
|
|
15
|
+
private static instance: UploadManager;
|
|
16
|
+
private storageProviders: Map<string, StorageProvider> = new Map();
|
|
17
|
+
private defaultStorageProvider: string = "local";
|
|
18
|
+
private fileValidator: FileValidator;
|
|
19
|
+
private globalConfig: UploadConfiguration;
|
|
20
|
+
|
|
21
|
+
private constructor() {
|
|
22
|
+
this.fileValidator = new FileValidator();
|
|
23
|
+
this.globalConfig = this.getDefaultConfiguration();
|
|
24
|
+
this.initializeDefaultProviders();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static getInstance(): UploadManager {
|
|
28
|
+
if (!UploadManager.instance) {
|
|
29
|
+
UploadManager.instance = new UploadManager();
|
|
30
|
+
}
|
|
31
|
+
return UploadManager.instance;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register a storage provider
|
|
36
|
+
*/
|
|
37
|
+
public registerStorageProvider(name: string, provider: StorageProvider): void {
|
|
38
|
+
logger.info(`Registering storage provider: ${name}`);
|
|
39
|
+
this.storageProviders.set(name, provider);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set the default storage provider
|
|
44
|
+
*/
|
|
45
|
+
public setDefaultStorageProvider(name: string): void {
|
|
46
|
+
if (!this.storageProviders.has(name)) {
|
|
47
|
+
throw new Error(`Storage provider '${name}' not found`);
|
|
48
|
+
}
|
|
49
|
+
this.defaultStorageProvider = name;
|
|
50
|
+
logger.info(`Default storage provider set to: ${name}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get storage provider by name
|
|
55
|
+
*/
|
|
56
|
+
public getStorageProvider(name?: string): StorageProvider {
|
|
57
|
+
const providerName = name || this.defaultStorageProvider;
|
|
58
|
+
const provider = this.storageProviders.get(providerName);
|
|
59
|
+
if (!provider) {
|
|
60
|
+
throw new Error(`Storage provider '${providerName}' not found`);
|
|
61
|
+
}
|
|
62
|
+
return provider;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Process file upload
|
|
67
|
+
*/
|
|
68
|
+
public async uploadFile(
|
|
69
|
+
file: File,
|
|
70
|
+
config?: Partial<UploadConfiguration>,
|
|
71
|
+
storageProvider?: string
|
|
72
|
+
): Promise<UploadResult> {
|
|
73
|
+
const uploadId = uuidv7();
|
|
74
|
+
const mergedConfig = { ...this.globalConfig, ...config };
|
|
75
|
+
|
|
76
|
+
logger.info(`Processing upload ${uploadId} for file: ${file.name}`);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Validate file
|
|
80
|
+
const validation = await this.fileValidator.validate(file, mergedConfig);
|
|
81
|
+
if (!validation.valid) {
|
|
82
|
+
const error: UploadError = {
|
|
83
|
+
uploadId,
|
|
84
|
+
code: "VALIDATION_FAILED",
|
|
85
|
+
message: validation.errors.join(", "),
|
|
86
|
+
details: { validationErrors: validation.errors }
|
|
87
|
+
};
|
|
88
|
+
logger.warn(`Upload ${uploadId} validation failed: ${validation.errors.join(', ')}`);
|
|
89
|
+
return { success: false, error };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get storage provider
|
|
93
|
+
const provider = this.getStorageProvider(storageProvider);
|
|
94
|
+
|
|
95
|
+
// Generate file metadata
|
|
96
|
+
const metadata = await this.generateFileMetadata(file, uploadId, mergedConfig);
|
|
97
|
+
|
|
98
|
+
// Store file
|
|
99
|
+
const storeResult = await provider.store(file, metadata, mergedConfig);
|
|
100
|
+
|
|
101
|
+
const result: UploadResult = {
|
|
102
|
+
success: true,
|
|
103
|
+
uploadId,
|
|
104
|
+
fileName: metadata.fileName,
|
|
105
|
+
originalFileName: file.name,
|
|
106
|
+
mimeType: file.type,
|
|
107
|
+
size: file.size,
|
|
108
|
+
path: storeResult.path,
|
|
109
|
+
url: storeResult.url,
|
|
110
|
+
metadata: storeResult.metadata || {}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
logger.info(`Upload ${uploadId} completed successfully`);
|
|
114
|
+
return result;
|
|
115
|
+
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const uploadError: UploadError = {
|
|
118
|
+
uploadId,
|
|
119
|
+
code: "UPLOAD_FAILED",
|
|
120
|
+
message: error instanceof Error ? error.message : "Unknown error occurred",
|
|
121
|
+
details: { originalError: error }
|
|
122
|
+
};
|
|
123
|
+
logger.error(`Upload ${uploadId} failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
124
|
+
return { success: false, error: uploadError };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Process multiple file uploads
|
|
130
|
+
*/
|
|
131
|
+
public async uploadFiles(
|
|
132
|
+
files: File[],
|
|
133
|
+
config?: Partial<UploadConfiguration>,
|
|
134
|
+
storageProvider?: string
|
|
135
|
+
): Promise<UploadResult[]> {
|
|
136
|
+
logger.info(`Processing batch upload of ${files.length} files`);
|
|
137
|
+
|
|
138
|
+
const uploadPromises = files.map(file =>
|
|
139
|
+
this.uploadFile(file, config, storageProvider)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return await Promise.all(uploadPromises);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Delete uploaded file
|
|
147
|
+
*/
|
|
148
|
+
public async deleteFile(path: string, storageProvider?: string): Promise<boolean> {
|
|
149
|
+
try {
|
|
150
|
+
const provider = this.getStorageProvider(storageProvider);
|
|
151
|
+
const success = await provider.delete(path);
|
|
152
|
+
logger.info(`File deleted: ${path}`);
|
|
153
|
+
return success;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error(`Failed to delete file ${path}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get file URL
|
|
162
|
+
*/
|
|
163
|
+
public async getFileUrl(path: string, storageProvider?: string): Promise<string | null> {
|
|
164
|
+
try {
|
|
165
|
+
const provider = this.getStorageProvider(storageProvider);
|
|
166
|
+
return await provider.getUrl(path);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error(`Failed to get URL for file ${path}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Update global configuration
|
|
175
|
+
*/
|
|
176
|
+
public updateConfiguration(config: Partial<UploadConfiguration>): void {
|
|
177
|
+
this.globalConfig = { ...this.globalConfig, ...config };
|
|
178
|
+
logger.info("Upload configuration updated");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get current configuration
|
|
183
|
+
*/
|
|
184
|
+
public getConfiguration(): UploadConfiguration {
|
|
185
|
+
return { ...this.globalConfig };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async initializeDefaultProviders(): Promise<void> {
|
|
189
|
+
// Register default local storage provider
|
|
190
|
+
const localProvider = new LocalStorageProvider();
|
|
191
|
+
await localProvider.initialize();
|
|
192
|
+
this.registerStorageProvider("local", localProvider);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private getDefaultConfiguration(): UploadConfiguration {
|
|
196
|
+
return {
|
|
197
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
198
|
+
allowedMimeTypes: [
|
|
199
|
+
"image/jpeg",
|
|
200
|
+
"image/png",
|
|
201
|
+
"image/gif",
|
|
202
|
+
"image/webp"
|
|
203
|
+
],
|
|
204
|
+
allowedExtensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
|
|
205
|
+
validateFileSignature: true,
|
|
206
|
+
sanitizeFileName: true,
|
|
207
|
+
preserveOriginalName: false,
|
|
208
|
+
generateThumbnails: false,
|
|
209
|
+
uploadPath: "uploads",
|
|
210
|
+
namingStrategy: "uuid"
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async generateFileMetadata(
|
|
215
|
+
file: File,
|
|
216
|
+
uploadId: string,
|
|
217
|
+
config: UploadConfiguration
|
|
218
|
+
): Promise<FileMetadata> {
|
|
219
|
+
const extension = this.getFileExtension(file.name);
|
|
220
|
+
|
|
221
|
+
let fileName: string;
|
|
222
|
+
switch (config.namingStrategy) {
|
|
223
|
+
case "uuid":
|
|
224
|
+
fileName = `${uploadId}${extension}`;
|
|
225
|
+
break;
|
|
226
|
+
case "timestamp":
|
|
227
|
+
fileName = `${Date.now()}_${this.sanitizeFileName(file.name)}`;
|
|
228
|
+
break;
|
|
229
|
+
case "original":
|
|
230
|
+
fileName = config.sanitizeFileName ?
|
|
231
|
+
this.sanitizeFileName(file.name) : file.name;
|
|
232
|
+
break;
|
|
233
|
+
default:
|
|
234
|
+
fileName = `${uploadId}${extension}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
uploadId,
|
|
239
|
+
fileName,
|
|
240
|
+
originalFileName: file.name,
|
|
241
|
+
mimeType: file.type,
|
|
242
|
+
size: file.size,
|
|
243
|
+
extension,
|
|
244
|
+
uploadedAt: new Date().toISOString()
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private getFileExtension(fileName: string): string {
|
|
249
|
+
const lastDot = fileName.lastIndexOf('.');
|
|
250
|
+
return lastDot > 0 ? fileName.slice(lastDot) : '';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private sanitizeFileName(fileName: string): string {
|
|
254
|
+
// Remove dangerous characters and normalize
|
|
255
|
+
return fileName
|
|
256
|
+
.replace(/[^a-zA-Z0-9.-]/g, '_')
|
|
257
|
+
.replace(/_{2,}/g, '_')
|
|
258
|
+
.replace(/^_+|_+$/g, '')
|
|
259
|
+
.toLowerCase();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { BaseComponent, Component, CompData } from "../Components";
|
|
2
|
+
import type { UploadComponentData } from "../../types/upload.types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UploadComponent - Stores file upload metadata in entities
|
|
6
|
+
* This component contains all essential information about uploaded files
|
|
7
|
+
*/
|
|
8
|
+
@Component
|
|
9
|
+
export class UploadComponent extends BaseComponent {
|
|
10
|
+
@CompData()
|
|
11
|
+
uploadId: string = "";
|
|
12
|
+
|
|
13
|
+
@CompData()
|
|
14
|
+
fileName: string = "";
|
|
15
|
+
|
|
16
|
+
@CompData()
|
|
17
|
+
originalFileName: string = "";
|
|
18
|
+
|
|
19
|
+
@CompData()
|
|
20
|
+
mimeType: string = "";
|
|
21
|
+
|
|
22
|
+
@CompData()
|
|
23
|
+
size: number = 0;
|
|
24
|
+
|
|
25
|
+
@CompData()
|
|
26
|
+
path: string = "";
|
|
27
|
+
|
|
28
|
+
@CompData()
|
|
29
|
+
url: string = "";
|
|
30
|
+
|
|
31
|
+
@CompData()
|
|
32
|
+
uploadedAt: string = "";
|
|
33
|
+
|
|
34
|
+
@CompData()
|
|
35
|
+
metadata: string = "{}"; // JSON string for additional metadata
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set upload data from UploadResult
|
|
39
|
+
*/
|
|
40
|
+
public setUploadData(data: UploadComponentData): void {
|
|
41
|
+
this.uploadId = data.uploadId;
|
|
42
|
+
this.fileName = data.fileName;
|
|
43
|
+
this.originalFileName = data.originalFileName;
|
|
44
|
+
this.mimeType = data.mimeType;
|
|
45
|
+
this.size = data.size;
|
|
46
|
+
this.path = data.path;
|
|
47
|
+
this.url = data.url;
|
|
48
|
+
this.uploadedAt = data.uploadedAt;
|
|
49
|
+
this.metadata = JSON.stringify(data.metadata || {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get parsed metadata
|
|
54
|
+
*/
|
|
55
|
+
public getMetadata(): Record<string, any> {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(this.metadata);
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Update metadata
|
|
65
|
+
*/
|
|
66
|
+
public updateMetadata(newMetadata: Record<string, any>): void {
|
|
67
|
+
const current = this.getMetadata();
|
|
68
|
+
this.metadata = JSON.stringify({ ...current, ...newMetadata });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if this is an image file
|
|
73
|
+
*/
|
|
74
|
+
public isImage(): boolean {
|
|
75
|
+
return this.mimeType.startsWith("image/");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if this is a document file
|
|
80
|
+
*/
|
|
81
|
+
public isDocument(): boolean {
|
|
82
|
+
const documentMimeTypes = [
|
|
83
|
+
"application/pdf",
|
|
84
|
+
"text/plain",
|
|
85
|
+
"application/msword",
|
|
86
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
87
|
+
];
|
|
88
|
+
return documentMimeTypes.includes(this.mimeType);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get file extension
|
|
93
|
+
*/
|
|
94
|
+
public getExtension(): string {
|
|
95
|
+
const lastDot = this.fileName.lastIndexOf('.');
|
|
96
|
+
return lastDot > 0 ? this.fileName.slice(lastDot) : '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get human-readable file size
|
|
101
|
+
*/
|
|
102
|
+
public getHumanReadableSize(): string {
|
|
103
|
+
const bytes = this.size;
|
|
104
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
105
|
+
if (bytes === 0) return '0 Bytes';
|
|
106
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
107
|
+
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get upload age in days
|
|
112
|
+
*/
|
|
113
|
+
public getUploadAge(): number {
|
|
114
|
+
const uploadDate = new Date(this.uploadedAt);
|
|
115
|
+
const now = new Date();
|
|
116
|
+
const diffTime = Math.abs(now.getTime() - uploadDate.getTime());
|
|
117
|
+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* ImageMetadataComponent - Extended metadata for image files
|
|
123
|
+
*/
|
|
124
|
+
@Component
|
|
125
|
+
export class ImageMetadataComponent extends BaseComponent {
|
|
126
|
+
@CompData()
|
|
127
|
+
width: number = 0;
|
|
128
|
+
|
|
129
|
+
@CompData()
|
|
130
|
+
height: number = 0;
|
|
131
|
+
|
|
132
|
+
@CompData()
|
|
133
|
+
colorDepth: number = 0;
|
|
134
|
+
|
|
135
|
+
@CompData()
|
|
136
|
+
hasAlpha: boolean = false;
|
|
137
|
+
|
|
138
|
+
@CompData()
|
|
139
|
+
isAnimated: boolean = false;
|
|
140
|
+
|
|
141
|
+
@CompData()
|
|
142
|
+
thumbnails: string = "[]"; // JSON array of thumbnail paths
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Set image dimensions
|
|
146
|
+
*/
|
|
147
|
+
public setDimensions(width: number, height: number): void {
|
|
148
|
+
this.width = width;
|
|
149
|
+
this.height = height;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get aspect ratio
|
|
154
|
+
*/
|
|
155
|
+
public getAspectRatio(): number {
|
|
156
|
+
return this.height > 0 ? this.width / this.height : 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get thumbnail paths
|
|
161
|
+
*/
|
|
162
|
+
public getThumbnails(): Array<{ size: string; url: string; path: string }> {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(this.thumbnails);
|
|
165
|
+
} catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add thumbnail
|
|
172
|
+
*/
|
|
173
|
+
public addThumbnail(thumbnail: { size: string; url: string; path: string }): void {
|
|
174
|
+
const thumbnails = this.getThumbnails();
|
|
175
|
+
thumbnails.push(thumbnail);
|
|
176
|
+
this.thumbnails = JSON.stringify(thumbnails);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if image is landscape
|
|
181
|
+
*/
|
|
182
|
+
public isLandscape(): boolean {
|
|
183
|
+
return this.width > this.height;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if image is portrait
|
|
188
|
+
*/
|
|
189
|
+
public isPortrait(): boolean {
|
|
190
|
+
return this.height > this.width;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if image is square
|
|
195
|
+
*/
|
|
196
|
+
public isSquare(): boolean {
|
|
197
|
+
return this.width === this.height;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get megapixels
|
|
202
|
+
*/
|
|
203
|
+
public getMegapixels(): number {
|
|
204
|
+
return Math.round((this.width * this.height) / 1000000 * 100) / 100;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import EntityHookManager from "../EntityHookManager";
|
|
2
|
+
import type { EntityEvent, ComponentEvent } from "../events/EntityLifecycleEvents";
|
|
3
|
+
import type { HookOptions, ComponentTargetConfig } from "../EntityHookManager";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decorator for registering entity lifecycle hooks
|
|
7
|
+
* @param eventType The entity event type to hook into
|
|
8
|
+
* @param options Hook registration options
|
|
9
|
+
*/
|
|
10
|
+
export function EntityHook(eventType: EntityEvent['eventType'], options: HookOptions = {}) {
|
|
11
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
12
|
+
const originalMethod = descriptor.value;
|
|
13
|
+
|
|
14
|
+
// Store hook info for later registration
|
|
15
|
+
if (!target.constructor.__entityHooks) {
|
|
16
|
+
target.constructor.__entityHooks = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
target.constructor.__entityHooks.push({
|
|
20
|
+
eventType,
|
|
21
|
+
methodName: propertyKey,
|
|
22
|
+
options
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Replace method to ensure it can be called normally
|
|
26
|
+
descriptor.value = function (...args: any[]) {
|
|
27
|
+
return originalMethod.apply(this, args);
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decorator for registering component lifecycle hooks
|
|
34
|
+
* @param eventType The component event type to hook into
|
|
35
|
+
* @param options Hook registration options
|
|
36
|
+
*/
|
|
37
|
+
export function ComponentHook(eventType: ComponentEvent['eventType'], options: HookOptions = {}) {
|
|
38
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
39
|
+
const originalMethod = descriptor.value;
|
|
40
|
+
|
|
41
|
+
// Store hook info for later registration
|
|
42
|
+
if (!target.constructor.__componentHooks) {
|
|
43
|
+
target.constructor.__componentHooks = [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
target.constructor.__componentHooks.push({
|
|
47
|
+
eventType,
|
|
48
|
+
methodName: propertyKey,
|
|
49
|
+
options
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Replace method to ensure it can be called normally
|
|
53
|
+
descriptor.value = function (...args: any[]) {
|
|
54
|
+
return originalMethod.apply(this, args);
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Decorator for registering hooks for all lifecycle events
|
|
61
|
+
* @param options Hook registration options
|
|
62
|
+
*/
|
|
63
|
+
export function LifecycleHook(options: HookOptions = {}) {
|
|
64
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
65
|
+
const originalMethod = descriptor.value;
|
|
66
|
+
|
|
67
|
+
// Store hook info for later registration
|
|
68
|
+
if (!target.constructor.__lifecycleHooks) {
|
|
69
|
+
target.constructor.__lifecycleHooks = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
target.constructor.__lifecycleHooks.push({
|
|
73
|
+
methodName: propertyKey,
|
|
74
|
+
options
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Replace method to ensure it can be called normally
|
|
78
|
+
descriptor.value = function (...args: any[]) {
|
|
79
|
+
return originalMethod.apply(this, args);
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decorator for registering component-targeted entity lifecycle hooks
|
|
86
|
+
* @param eventType The entity event type to hook into
|
|
87
|
+
* @param componentTarget Component targeting configuration
|
|
88
|
+
* @param options Additional hook registration options
|
|
89
|
+
*/
|
|
90
|
+
export function ComponentTargetHook(
|
|
91
|
+
eventType: EntityEvent['eventType'],
|
|
92
|
+
componentTarget: ComponentTargetConfig,
|
|
93
|
+
options: Omit<HookOptions, 'componentTarget'> = {}
|
|
94
|
+
) {
|
|
95
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
96
|
+
const originalMethod = descriptor.value;
|
|
97
|
+
|
|
98
|
+
// Store hook info for later registration
|
|
99
|
+
if (!target.constructor.__componentTargetHooks) {
|
|
100
|
+
target.constructor.__componentTargetHooks = [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
target.constructor.__componentTargetHooks.push({
|
|
104
|
+
eventType,
|
|
105
|
+
methodName: propertyKey,
|
|
106
|
+
componentTarget,
|
|
107
|
+
options
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Replace method to ensure it can be called normally
|
|
111
|
+
descriptor.value = function (...args: any[]) {
|
|
112
|
+
return originalMethod.apply(this, args);
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Register all decorated hooks for a service class
|
|
119
|
+
* Call this method after instantiating a service to register its decorated hooks
|
|
120
|
+
* @param serviceInstance The service instance to register hooks for
|
|
121
|
+
*/
|
|
122
|
+
export function registerDecoratedHooks(serviceInstance: any): void {
|
|
123
|
+
const constructor = serviceInstance.constructor;
|
|
124
|
+
|
|
125
|
+
// Register entity hooks
|
|
126
|
+
if (constructor.__entityHooks) {
|
|
127
|
+
for (const hookInfo of constructor.__entityHooks) {
|
|
128
|
+
const hookMethod = serviceInstance[hookInfo.methodName].bind(serviceInstance);
|
|
129
|
+
|
|
130
|
+
EntityHookManager.registerEntityHook(
|
|
131
|
+
hookInfo.eventType,
|
|
132
|
+
hookMethod,
|
|
133
|
+
hookInfo.options
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Register component hooks
|
|
139
|
+
if (constructor.__componentHooks) {
|
|
140
|
+
for (const hookInfo of constructor.__componentHooks) {
|
|
141
|
+
const hookMethod = serviceInstance[hookInfo.methodName].bind(serviceInstance);
|
|
142
|
+
|
|
143
|
+
EntityHookManager.registerComponentHook(
|
|
144
|
+
hookInfo.eventType,
|
|
145
|
+
hookMethod,
|
|
146
|
+
hookInfo.options
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Register component target hooks
|
|
152
|
+
if (constructor.__componentTargetHooks) {
|
|
153
|
+
for (const hookInfo of constructor.__componentTargetHooks) {
|
|
154
|
+
const hookMethod = serviceInstance[hookInfo.methodName].bind(serviceInstance);
|
|
155
|
+
|
|
156
|
+
EntityHookManager.registerEntityHook(
|
|
157
|
+
hookInfo.eventType,
|
|
158
|
+
hookMethod,
|
|
159
|
+
{
|
|
160
|
+
...hookInfo.options,
|
|
161
|
+
componentTarget: hookInfo.componentTarget
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Register lifecycle hooks
|
|
168
|
+
if (constructor.__lifecycleHooks) {
|
|
169
|
+
for (const hookInfo of constructor.__lifecycleHooks) {
|
|
170
|
+
const hookMethod = serviceInstance[hookInfo.methodName].bind(serviceInstance);
|
|
171
|
+
|
|
172
|
+
EntityHookManager.registerLifecycleHook(
|
|
173
|
+
hookMethod,
|
|
174
|
+
hookInfo.options
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Unregister all decorated hooks for a service class
|
|
182
|
+
* Call this method before destroying a service to clean up its hooks
|
|
183
|
+
* @param serviceInstance The service instance to unregister hooks for
|
|
184
|
+
*/
|
|
185
|
+
export function unregisterDecoratedHooks(serviceInstance: any): void {
|
|
186
|
+
// Note: This is a simplified implementation
|
|
187
|
+
// In a production system, you'd want to track hook IDs during registration
|
|
188
|
+
// and use them for targeted removal here
|
|
189
|
+
console.warn('unregisterDecoratedHooks is not fully implemented. Use EntityHookManager.removeHook() for individual hook removal.');
|
|
190
|
+
}
|