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,495 @@
1
+ import { EntityHook, ComponentHook } from "../../core/decorators/EntityHooks";
2
+ import { EntityCreatedEvent, EntityUpdatedEvent, EntityDeletedEvent, ComponentAddedEvent, ComponentUpdatedEvent, ComponentRemovedEvent } from "../../core/events/EntityLifecycleEvents";
3
+ import { Entity } from "../../core/Entity";
4
+ import { logger } from "../../core/Logger";
5
+
6
+ export interface AuditLogEntry {
7
+ id: string;
8
+ timestamp: Date;
9
+ action: 'create' | 'update' | 'delete' | 'add_component' | 'update_component' | 'remove_component';
10
+ entityId: string;
11
+ entityType?: string;
12
+ componentType?: string;
13
+ userId?: string;
14
+ oldData?: any;
15
+ newData?: any;
16
+ metadata?: Record<string, any>;
17
+ }
18
+
19
+ export interface AuditLoggerConfig {
20
+ enabled: boolean;
21
+ level: 'debug' | 'info' | 'warn' | 'error';
22
+ storage: 'memory' | 'database' | 'file' | 'external';
23
+ includeData: boolean;
24
+ maxEntries: number;
25
+ retentionDays: number;
26
+ }
27
+
28
+ /**
29
+ * AuditLogger - Comprehensive audit logging for entity lifecycle events
30
+ *
31
+ * Features:
32
+ * - Logs all entity and component lifecycle events
33
+ * - Multiple storage backends (memory, database, file, external)
34
+ * - Configurable data inclusion
35
+ * - Automatic cleanup of old entries
36
+ * - Performance monitoring
37
+ * - User context tracking
38
+ */
39
+ export class AuditLogger {
40
+ private logs: AuditLogEntry[] = [];
41
+ private config: AuditLoggerConfig;
42
+ private cleanupInterval: NodeJS.Timeout | null = null;
43
+
44
+ constructor(config: Partial<AuditLoggerConfig> = {}) {
45
+ this.config = {
46
+ enabled: true,
47
+ level: 'info',
48
+ storage: 'memory',
49
+ includeData: true,
50
+ maxEntries: 10000,
51
+ retentionDays: 30,
52
+ ...config
53
+ };
54
+
55
+ if (this.config.enabled) {
56
+ this.startCleanupInterval();
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Log entity creation events
62
+ */
63
+ @EntityHook("entity.created")
64
+ async handleEntityCreated(event: EntityCreatedEvent) {
65
+ if (!this.config.enabled) return;
66
+
67
+ const entry: AuditLogEntry = {
68
+ id: this.generateId(),
69
+ timestamp: new Date(),
70
+ action: 'create',
71
+ entityId: event.getEntity().id,
72
+ entityType: this.getEntityType(event.getEntity()),
73
+ userId: this.getCurrentUserId(),
74
+ newData: this.config.includeData ? await this.extractEntityData(event.getEntity()) : undefined,
75
+ metadata: {
76
+ isNew: event.isNew,
77
+ source: 'entity_hook'
78
+ }
79
+ };
80
+
81
+ await this.storeLogEntry(entry);
82
+ this.log('info', `Entity created: ${entry.entityId}`, entry);
83
+ }
84
+
85
+ /**
86
+ * Log entity update events
87
+ */
88
+ @EntityHook("entity.updated")
89
+ async handleEntityUpdated(event: EntityUpdatedEvent) {
90
+ if (!this.config.enabled) return;
91
+
92
+ const entry: AuditLogEntry = {
93
+ id: this.generateId(),
94
+ timestamp: new Date(),
95
+ action: 'update',
96
+ entityId: event.getEntity().id,
97
+ entityType: this.getEntityType(event.getEntity()),
98
+ userId: this.getCurrentUserId(),
99
+ metadata: {
100
+ changedComponents: event.getChangedComponents(),
101
+ source: 'entity_hook'
102
+ }
103
+ };
104
+
105
+ await this.storeLogEntry(entry);
106
+ this.log('info', `Entity updated: ${entry.entityId}`, entry);
107
+ }
108
+
109
+ /**
110
+ * Log entity deletion events
111
+ */
112
+ @EntityHook("entity.deleted")
113
+ async handleEntityDeleted(event: EntityDeletedEvent) {
114
+ if (!this.config.enabled) return;
115
+
116
+ const entry: AuditLogEntry = {
117
+ id: this.generateId(),
118
+ timestamp: new Date(),
119
+ action: 'delete',
120
+ entityId: event.getEntity().id,
121
+ entityType: this.getEntityType(event.getEntity()),
122
+ userId: this.getCurrentUserId(),
123
+ oldData: this.config.includeData ? await this.extractEntityData(event.getEntity()) : undefined,
124
+ metadata: {
125
+ isSoftDelete: event.isSoftDelete,
126
+ source: 'entity_hook'
127
+ }
128
+ };
129
+
130
+ await this.storeLogEntry(entry);
131
+ this.log('info', `Entity deleted: ${entry.entityId}`, entry);
132
+ }
133
+
134
+ /**
135
+ * Log component addition events
136
+ */
137
+ @ComponentHook("component.added")
138
+ async handleComponentAdded(event: ComponentAddedEvent) {
139
+ if (!this.config.enabled) return;
140
+
141
+ const entry: AuditLogEntry = {
142
+ id: this.generateId(),
143
+ timestamp: new Date(),
144
+ action: 'add_component',
145
+ entityId: event.getEntity().id,
146
+ entityType: this.getEntityType(event.getEntity()),
147
+ componentType: event.getComponentType(),
148
+ userId: this.getCurrentUserId(),
149
+ newData: this.config.includeData ? event.getComponent().data() : undefined,
150
+ metadata: {
151
+ source: 'component_hook'
152
+ }
153
+ };
154
+
155
+ await this.storeLogEntry(entry);
156
+ this.log('debug', `Component added: ${entry.componentType} to ${entry.entityId}`, entry);
157
+ }
158
+
159
+ /**
160
+ * Log component update events
161
+ */
162
+ @ComponentHook("component.updated")
163
+ async handleComponentUpdated(event: ComponentUpdatedEvent) {
164
+ if (!this.config.enabled) return;
165
+
166
+ const entry: AuditLogEntry = {
167
+ id: this.generateId(),
168
+ timestamp: new Date(),
169
+ action: 'update_component',
170
+ entityId: event.getEntity().id,
171
+ entityType: this.getEntityType(event.getEntity()),
172
+ componentType: event.getComponentType(),
173
+ userId: this.getCurrentUserId(),
174
+ oldData: this.config.includeData ? event.getOldData() : undefined,
175
+ newData: this.config.includeData ? event.getNewData() : undefined,
176
+ metadata: {
177
+ source: 'component_hook'
178
+ }
179
+ };
180
+
181
+ await this.storeLogEntry(entry);
182
+ this.log('debug', `Component updated: ${entry.componentType} on ${entry.entityId}`, entry);
183
+ }
184
+
185
+ /**
186
+ * Log component removal events
187
+ */
188
+ @ComponentHook("component.removed")
189
+ async handleComponentRemoved(event: ComponentRemovedEvent) {
190
+ if (!this.config.enabled) return;
191
+
192
+ const entry: AuditLogEntry = {
193
+ id: this.generateId(),
194
+ timestamp: new Date(),
195
+ action: 'remove_component',
196
+ entityId: event.getEntity().id,
197
+ entityType: this.getEntityType(event.getEntity()),
198
+ componentType: event.getComponentType(),
199
+ userId: this.getCurrentUserId(),
200
+ oldData: this.config.includeData ? event.getComponent().data() : undefined,
201
+ metadata: {
202
+ source: 'component_hook'
203
+ }
204
+ };
205
+
206
+ await this.storeLogEntry(entry);
207
+ this.log('debug', `Component removed: ${entry.componentType} from ${entry.entityId}`, entry);
208
+ }
209
+
210
+ /**
211
+ * Get all audit logs
212
+ */
213
+ getLogs(filter?: {
214
+ entityId?: string;
215
+ action?: string;
216
+ userId?: string;
217
+ since?: Date;
218
+ until?: Date;
219
+ }): AuditLogEntry[] {
220
+ let filteredLogs = [...this.logs];
221
+
222
+ if (filter) {
223
+ if (filter.entityId) {
224
+ filteredLogs = filteredLogs.filter(log => log.entityId === filter.entityId);
225
+ }
226
+ if (filter.action) {
227
+ filteredLogs = filteredLogs.filter(log => log.action === filter.action);
228
+ }
229
+ if (filter.userId) {
230
+ filteredLogs = filteredLogs.filter(log => log.userId === filter.userId);
231
+ }
232
+ if (filter.since) {
233
+ filteredLogs = filteredLogs.filter(log => log.timestamp >= filter.since!);
234
+ }
235
+ if (filter.until) {
236
+ filteredLogs = filteredLogs.filter(log => log.timestamp <= filter.until!);
237
+ }
238
+ }
239
+
240
+ return filteredLogs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
241
+ }
242
+
243
+ /**
244
+ * Get audit logs for a specific entity
245
+ */
246
+ getEntityLogs(entityId: string): AuditLogEntry[] {
247
+ return this.getLogs({ entityId });
248
+ }
249
+
250
+ /**
251
+ * Get audit logs for a specific user
252
+ */
253
+ getUserLogs(userId: string): AuditLogEntry[] {
254
+ return this.getLogs({ userId });
255
+ }
256
+
257
+ /**
258
+ * Clear old audit logs based on retention policy
259
+ */
260
+ async clearOldLogs(): Promise<void> {
261
+ const cutoffDate = new Date();
262
+ cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
263
+
264
+ const initialCount = this.logs.length;
265
+ this.logs = this.logs.filter(log => log.timestamp >= cutoffDate);
266
+
267
+ const removedCount = initialCount - this.logs.length;
268
+ if (removedCount > 0) {
269
+ this.log('info', `Cleared ${removedCount} old audit log entries`);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Export audit logs to different formats
275
+ */
276
+ exportLogs(format: 'json' | 'csv' = 'json'): string {
277
+ switch (format) {
278
+ case 'json':
279
+ return JSON.stringify(this.logs, null, 2);
280
+ case 'csv':
281
+ return this.convertToCSV(this.logs);
282
+ default:
283
+ throw new Error(`Unsupported export format: ${format}`);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Get audit statistics
289
+ */
290
+ getStatistics(): {
291
+ totalEntries: number;
292
+ entriesByAction: Record<string, number>;
293
+ entriesByEntityType: Record<string, number>;
294
+ oldestEntry: Date | null;
295
+ newestEntry: Date | null;
296
+ averageEntriesPerDay: number;
297
+ } {
298
+ const stats = {
299
+ totalEntries: this.logs.length,
300
+ entriesByAction: {} as Record<string, number>,
301
+ entriesByEntityType: {} as Record<string, number>,
302
+ oldestEntry: null as Date | null,
303
+ newestEntry: null as Date | null,
304
+ averageEntriesPerDay: 0
305
+ };
306
+
307
+ if (this.logs.length === 0) return stats;
308
+
309
+ // Calculate distributions
310
+ for (const log of this.logs) {
311
+ stats.entriesByAction[log.action] = (stats.entriesByAction[log.action] || 0) + 1;
312
+ if (log.entityType) {
313
+ stats.entriesByEntityType[log.entityType] = (stats.entriesByEntityType[log.entityType] || 0) + 1;
314
+ }
315
+ }
316
+
317
+ // Calculate date range
318
+ const timestamps = this.logs.map(log => log.timestamp.getTime()).sort();
319
+ stats.oldestEntry = new Date(timestamps[0]!);
320
+ stats.newestEntry = new Date(timestamps[timestamps.length - 1]!);
321
+
322
+ // Calculate average entries per day
323
+ const daysDiff = (stats.newestEntry.getTime() - stats.oldestEntry.getTime()) / (1000 * 60 * 60 * 24);
324
+ stats.averageEntriesPerDay = daysDiff > 0 ? stats.totalEntries / daysDiff : stats.totalEntries;
325
+
326
+ return stats;
327
+ }
328
+
329
+ /**
330
+ * Store log entry based on configured storage backend
331
+ */
332
+ private async storeLogEntry(entry: AuditLogEntry): Promise<void> {
333
+ try {
334
+ switch (this.config.storage) {
335
+ case 'memory':
336
+ this.storeInMemory(entry);
337
+ break;
338
+ case 'database':
339
+ await this.storeInDatabase(entry);
340
+ break;
341
+ case 'file':
342
+ await this.storeInFile(entry);
343
+ break;
344
+ case 'external':
345
+ await this.storeExternally(entry);
346
+ break;
347
+ default:
348
+ throw new Error(`Unsupported storage backend: ${this.config.storage}`);
349
+ }
350
+ } catch (error) {
351
+ this.log('error', `Failed to store audit log entry: ${error}`);
352
+ // Fallback to memory storage
353
+ this.storeInMemory(entry);
354
+ }
355
+ }
356
+
357
+ private storeInMemory(entry: AuditLogEntry): void {
358
+ this.logs.push(entry);
359
+
360
+ // Enforce max entries limit
361
+ if (this.logs.length > this.config.maxEntries) {
362
+ this.logs = this.logs.slice(-this.config.maxEntries);
363
+ }
364
+ }
365
+
366
+ private async storeInDatabase(entry: AuditLogEntry): Promise<void> {
367
+ // Implementation for database storage
368
+ // This would integrate with your database layer
369
+ throw new Error("Database storage not implemented - extend this method");
370
+ }
371
+
372
+ private async storeInFile(entry: AuditLogEntry): Promise<void> {
373
+ // Implementation for file storage
374
+ // This would write to log files
375
+ throw new Error("File storage not implemented - extend this method");
376
+ }
377
+
378
+ private async storeExternally(entry: AuditLogEntry): Promise<void> {
379
+ // Implementation for external service (e.g., Logstash, Splunk)
380
+ // This would send to external logging service
381
+ throw new Error("External storage not implemented - extend this method");
382
+ }
383
+
384
+ private startCleanupInterval(): void {
385
+ // Run cleanup daily
386
+ this.cleanupInterval = setInterval(() => {
387
+ this.clearOldLogs().catch(error => {
388
+ this.log('error', `Failed to clear old audit logs: ${error}`);
389
+ });
390
+ }, 24 * 60 * 60 * 1000); // 24 hours
391
+ }
392
+
393
+ private stopCleanupInterval(): void {
394
+ if (this.cleanupInterval) {
395
+ clearInterval(this.cleanupInterval);
396
+ this.cleanupInterval = null;
397
+ }
398
+ }
399
+
400
+ private generateId(): string {
401
+ return `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
402
+ }
403
+
404
+ private getEntityType(entity: Entity): string | undefined {
405
+ // Try to determine entity type from components
406
+ // This is a simple implementation - you might want to enhance this
407
+ try {
408
+ const components = entity.componentList();
409
+ return components.length > 0 ? components[0]?.constructor.name : 'unknown';
410
+ } catch {
411
+ return 'unknown';
412
+ }
413
+ }
414
+
415
+ private getCurrentUserId(): string | undefined {
416
+ // Implementation to get current user from context
417
+ // This depends on your authentication system
418
+ return 'system'; // Default to system user
419
+ }
420
+
421
+ private async extractEntityData(entity: Entity): Promise<any> {
422
+ try {
423
+ const data: any = {};
424
+ const components = entity.componentList();
425
+
426
+ for (const component of components) {
427
+ try {
428
+ const componentData = await entity.get(component.constructor as any);
429
+ if (componentData) {
430
+ data[component.constructor.name] = componentData;
431
+ }
432
+ } catch (error) {
433
+ // Skip components that can't be retrieved
434
+ this.log('debug', `Could not extract data for component ${component.constructor.name}`);
435
+ }
436
+ }
437
+
438
+ return data;
439
+ } catch (error) {
440
+ this.log('warn', `Failed to extract entity data: ${error}`);
441
+ return { error: 'Failed to extract data' };
442
+ }
443
+ }
444
+
445
+ private convertToCSV(logs: AuditLogEntry[]): string {
446
+ const headers = [
447
+ 'id', 'timestamp', 'action', 'entityId', 'entityType',
448
+ 'componentType', 'userId', 'oldData', 'newData', 'metadata'
449
+ ];
450
+
451
+ const rows = logs.map(log => [
452
+ log.id,
453
+ log.timestamp.toISOString(),
454
+ log.action,
455
+ log.entityId,
456
+ log.entityType || '',
457
+ log.componentType || '',
458
+ log.userId || '',
459
+ log.oldData ? JSON.stringify(log.oldData) : '',
460
+ log.newData ? JSON.stringify(log.newData) : '',
461
+ log.metadata ? JSON.stringify(log.metadata) : ''
462
+ ]);
463
+
464
+ return [headers, ...rows]
465
+ .map(row => row.map(field => `"${field.replace(/"/g, '""')}"`).join(','))
466
+ .join('\n');
467
+ }
468
+
469
+ private log(level: string, message: string, data?: any): void {
470
+ const fullMessage = data ? `${message} ${JSON.stringify(data)}` : message;
471
+
472
+ switch (level) {
473
+ case 'debug':
474
+ logger.debug(fullMessage);
475
+ break;
476
+ case 'info':
477
+ logger.info(fullMessage);
478
+ break;
479
+ case 'warn':
480
+ logger.warn(fullMessage);
481
+ break;
482
+ case 'error':
483
+ logger.error(fullMessage);
484
+ break;
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Cleanup resources
490
+ */
491
+ destroy(): void {
492
+ this.stopCleanupInterval();
493
+ this.logs = [];
494
+ }
495
+ }
package/gql/Generator.ts CHANGED
@@ -1,72 +1,93 @@
1
1
  import { GraphQLSchema, GraphQLError } from "graphql";
2
- import {makeExecutableSchema} from "@graphql-tools/schema";
2
+ import { createSchema } from "graphql-yoga";
3
3
  import { logger as MainLogger } from "core/Logger";
4
+ import type { GraphQLType } from "./helpers";
4
5
  const logger = MainLogger.child({ scope: "GraphQLGenerator" });
5
- export interface GraphQLTypeMeta {
6
+ export interface GraphQLObjectTypeMeta {
6
7
  name: string;
7
- fields: Record<string, string>;
8
+ fields: Record<string, GraphQLType>;
8
9
  }
9
10
 
10
11
  export interface GraphQLOperationMeta {
11
12
  type: "Query" | "Mutation";
12
13
  name?: string;
13
- input?: Record<string, string>;
14
- output: Record<string, string> | string;
14
+ input?: Record<string, GraphQLType>;
15
+ output: GraphQLType | Record<string, GraphQLType>;
15
16
  }
16
17
 
17
18
  export interface GraphQLFieldMeta {
18
- type: string;
19
+ type: GraphQLType;
19
20
  field: string;
20
21
  }
21
22
 
22
- export function GraphQLType(meta: GraphQLTypeMeta) {
23
+ export function GraphQLObjectType(meta: GraphQLObjectTypeMeta) {
23
24
  return (target: any) => {
24
- target.__graphqlType = meta;
25
+ if (!target.__graphqlObjectType) target.__graphqlObjectType = [];
26
+ target.__graphqlObjectType.push(meta);
27
+ }
28
+ }
29
+
30
+ export function GraphQLScalarType(name: string) {
31
+ return (target: any) => {
32
+ if (!target.__graphqlScalarTypes) target.__graphqlScalarTypes = [];
33
+ target.__graphqlScalarTypes.push(name);
25
34
  }
26
35
  }
27
36
 
28
37
  export function GraphQLOperation(meta: GraphQLOperationMeta) {
29
- return function (target: any, context: ClassMethodDecoratorContext) {
38
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
30
39
  if (!target.__graphqlOperations) target.__graphqlOperations = [];
31
- const operationName = meta.name ?? context;
40
+ const operationName = meta.name ?? propertyKey;
32
41
  if (!operationName) {
33
- throw new Error("GraphQLOperation: Operation name is required (either meta.name or context.name must be defined)");
42
+ throw new Error("GraphQLOperation: Operation name is required (either meta.name or propertyKey must be defined)");
34
43
  }
35
- const operationMeta = { ...meta, name: operationName, propertyKey: context};
44
+ const operationMeta = { ...meta, name: operationName, propertyKey };
36
45
  target.__graphqlOperations.push(operationMeta);
37
46
  };
38
47
  }
39
48
 
40
49
  export function GraphQLField(meta: GraphQLFieldMeta) {
41
- return function (target: any, context: ClassMethodDecoratorContext) {
50
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
42
51
  if (!target.__graphqlFields) target.__graphqlFields = [];
43
- target.__graphqlFields.push({ ...meta, propertyKey: context });
52
+ target.__graphqlFields.push({ ...meta, propertyKey });
44
53
  };
45
54
  }
46
55
 
47
- export function generateGraphQLSchema(systems: any[]): { schema: GraphQLSchema | null; resolvers: any } {
48
- let typeDefs = "";
56
+ export function generateGraphQLSchema(services: any[]): { schema: GraphQLSchema | null; resolvers: any } {
57
+ let typeDefs = `
58
+ `;
59
+ const scalarTypes: Set<string> = new Set();
49
60
  const resolvers: any = { Query: {}, Mutation: {} };
50
61
  const queryFields: string[] = [];
51
62
  const mutationFields: string[] = [];
52
63
 
53
- systems.forEach(system => {
54
- logger.trace(`Processing system: ${system.constructor.name}`);
55
- if (system.constructor.__graphqlType) {
56
- const { name, fields } = system.constructor.__graphqlType;
57
- typeDefs += `type ${name} {\n${Object.entries(fields).map(([k, v]) => ` ${k}: ${v}`).join('\n')}\n}\n`;
64
+ services.forEach(service => {
65
+ logger.trace(`Processing service: ${service.constructor.name}`);
66
+ if(service.constructor.__graphqlScalarTypes) {
67
+ for (const scalarName of service.constructor.__graphqlScalarTypes) {
68
+ if (!scalarTypes.has(scalarName)) {
69
+ scalarTypes.add(scalarName);
70
+ typeDefs += `scalar ${scalarName}\n`;
71
+ }
72
+ }
73
+ }
74
+ if (service.constructor.__graphqlObjectType) {
75
+ for (const meta of service.constructor.__graphqlObjectType) {
76
+ const { name, fields } = meta;
77
+ typeDefs += `type ${name} {\n${Object.entries(fields).map(([k, v]) => ` ${k}: ${v}`).join('\n')}\n}\n`;
78
+ }
58
79
  }
59
- if (system.__graphqlOperations) {
60
- system.__graphqlOperations.forEach((op: any) => {
80
+ if (service.__graphqlOperations) {
81
+ service.__graphqlOperations.forEach((op: any) => {
61
82
  const { type, name, input, output, propertyKey } = op;
62
83
  let fieldDef = `${name}`;
63
84
  if (input) {
64
85
  const inputName = `${name}Input`;
65
86
  typeDefs += `input ${inputName} {\n${Object.entries(input).map(([k, v]) => ` ${k}: ${v}`).join('\n')}\n}\n`;
66
87
  fieldDef += `(input: ${inputName}!)`;
67
- resolvers[type][name] = async (_: any, args: any, context: any) => {
88
+ resolvers[type][name] = async (_: any, args: any, context: any, info: any) => {
68
89
  try {
69
- return await system[propertyKey](args.input || args, context);
90
+ return await service[propertyKey](args.input || args, context, info);
70
91
  } catch (error) {
71
92
  logger.error(`Error in ${type}.${name}:`);
72
93
  logger.error(error);
@@ -82,9 +103,9 @@ export function generateGraphQLSchema(systems: any[]): { schema: GraphQLSchema |
82
103
  }
83
104
  };
84
105
  } else {
85
- resolvers[type][name] = async (_: any, args: any, context: any) => {
106
+ resolvers[type][name] = async (_: any, args: any, context: any, info: any) => {
86
107
  try {
87
- return await system[propertyKey]({}, context);
108
+ return await service[propertyKey]({}, context, info);
88
109
  } catch (error) {
89
110
  logger.error(`Error in ${type}.${name}:`);
90
111
  logger.error(error);
@@ -117,14 +138,14 @@ export function generateGraphQLSchema(systems: any[]): { schema: GraphQLSchema |
117
138
  });
118
139
 
119
140
  // Process field resolvers
120
- systems.forEach(system => {
121
- if (system.__graphqlFields) {
122
- system.__graphqlFields.forEach((fieldMeta: any) => {
141
+ services.forEach(service => {
142
+ if (service.__graphqlFields) {
143
+ service.__graphqlFields.forEach((fieldMeta: any) => {
123
144
  const { type, field, propertyKey } = fieldMeta;
124
145
  if (!resolvers[type]) resolvers[type] = {};
125
- resolvers[type][field] = async (parent: any, args: any, context: any) => {
146
+ resolvers[type][field] = async (parent: any, args: any, context: any, info: any) => {
126
147
  try {
127
- return await system[propertyKey](parent, args, context);
148
+ return await service[propertyKey](parent, args, context, info);
128
149
  } catch (error) {
129
150
  logger.error(`Error in ${type}.${field}:`);
130
151
  logger.error(error);
@@ -152,8 +173,9 @@ export function generateGraphQLSchema(systems: any[]): { schema: GraphQLSchema |
152
173
 
153
174
  logger.trace(`System Type Defs: ${typeDefs}`);
154
175
  let schema : GraphQLSchema | null = null;
155
- if(typeDefs !== "") {
156
- schema = makeExecutableSchema({ typeDefs, resolvers });
176
+ // Check if typeDefs contains actual schema definitions, not just whitespace
177
+ if(typeDefs.trim() !== "" && (queryFields.length > 0 || mutationFields.length > 0 || scalarTypes.size > 0)) {
178
+ schema = createSchema({ typeDefs, resolvers });
157
179
  }
158
180
  return { schema, resolvers };
159
181
  }