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.
Files changed (85) 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 +119 -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 +159 -12
  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 +453 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +65 -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 +1 -1
  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/examples/hooks/README.md +228 -0
  52. package/examples/hooks/audit-logger.ts +495 -0
  53. package/gql/Generator.ts +56 -34
  54. package/gql/decorators/Upload.ts +176 -0
  55. package/gql/helpers.ts +67 -0
  56. package/gql/index.ts +55 -31
  57. package/gql/types.ts +1 -1
  58. package/index.ts +79 -11
  59. package/package.json +5 -4
  60. package/rest/Generator.ts +3 -0
  61. package/rest/index.ts +22 -0
  62. package/service/Service.ts +1 -1
  63. package/service/ServiceRegistry.ts +10 -6
  64. package/service/index.ts +12 -1
  65. package/tests/bench/insert.bench.ts +59 -0
  66. package/tests/bench/relations.bench.ts +269 -0
  67. package/tests/bench/sorting.bench.ts +415 -0
  68. package/tests/component-hooks.test.ts +1409 -0
  69. package/tests/component.test.ts +205 -0
  70. package/tests/errorHandling.test.ts +155 -0
  71. package/tests/hooks.test.ts +666 -0
  72. package/tests/query-sorting.test.ts +101 -0
  73. package/tests/relations.test.ts +169 -0
  74. package/tests/scheduler.test.ts +724 -0
  75. package/tsconfig.json +35 -34
  76. package/types/graphql.types.ts +87 -0
  77. package/types/hooks.types.ts +141 -0
  78. package/types/scheduler.types.ts +165 -0
  79. package/types/upload.types.ts +184 -0
  80. package/upload/index.ts +140 -0
  81. package/utils/UploadHelper.ts +305 -0
  82. package/utils/cronParser.ts +366 -0
  83. package/utils/errorMessages.ts +151 -0
  84. package/validate-docs.sh +90 -0
  85. 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
+ }