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,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 {
|
|
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
|
|
6
|
+
export interface GraphQLObjectTypeMeta {
|
|
6
7
|
name: string;
|
|
7
|
-
fields: Record<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,
|
|
14
|
-
output: Record<string,
|
|
14
|
+
input?: Record<string, GraphQLType>;
|
|
15
|
+
output: GraphQLType | Record<string, GraphQLType>;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface GraphQLFieldMeta {
|
|
18
|
-
type:
|
|
19
|
+
type: GraphQLType;
|
|
19
20
|
field: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export function
|
|
23
|
+
export function GraphQLObjectType(meta: GraphQLObjectTypeMeta) {
|
|
23
24
|
return (target: any) => {
|
|
24
|
-
target.
|
|
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,
|
|
38
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
30
39
|
if (!target.__graphqlOperations) target.__graphqlOperations = [];
|
|
31
|
-
const operationName = meta.name ??
|
|
40
|
+
const operationName = meta.name ?? propertyKey;
|
|
32
41
|
if (!operationName) {
|
|
33
|
-
throw new Error("GraphQLOperation: Operation name is required (either meta.name or
|
|
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
|
|
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,
|
|
50
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
42
51
|
if (!target.__graphqlFields) target.__graphqlFields = [];
|
|
43
|
-
target.__graphqlFields.push({ ...meta, propertyKey
|
|
52
|
+
target.__graphqlFields.push({ ...meta, propertyKey });
|
|
44
53
|
};
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
export function generateGraphQLSchema(
|
|
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
|
-
|
|
54
|
-
logger.trace(`Processing
|
|
55
|
-
if
|
|
56
|
-
const
|
|
57
|
-
|
|
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 (
|
|
60
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
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
|
|
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
|
|
156
|
-
|
|
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
|
}
|