bunsane 0.1.2 → 0.1.4

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 (49) hide show
  1. package/TODO.md +1 -1
  2. package/bun.lock +156 -150
  3. package/core/App.ts +188 -31
  4. package/core/ArcheType.ts +1044 -26
  5. package/core/ComponentRegistry.ts +172 -29
  6. package/core/Components.ts +102 -24
  7. package/core/Decorators.ts +0 -1
  8. package/core/Entity.ts +55 -7
  9. package/core/EntityInterface.ts +4 -0
  10. package/core/EntityManager.ts +4 -4
  11. package/core/Query.ts +169 -3
  12. package/core/RequestLoaders.ts +101 -12
  13. package/core/SchedulerManager.ts +3 -4
  14. package/core/metadata/definitions/ArcheType.ts +9 -0
  15. package/core/metadata/definitions/Component.ts +16 -0
  16. package/core/metadata/definitions/gqlObject.ts +10 -0
  17. package/core/metadata/getMetadataStorage.ts +14 -0
  18. package/core/metadata/index.ts +17 -0
  19. package/core/metadata/metadata-storage.ts +81 -0
  20. package/database/DatabaseHelper.ts +22 -20
  21. package/database/index.ts +6 -1
  22. package/database/sqlHelpers.ts +0 -2
  23. package/gql/ArchetypeOperations.ts +281 -0
  24. package/gql/Generator.ts +252 -62
  25. package/gql/helpers.ts +5 -5
  26. package/gql/index.ts +19 -17
  27. package/gql/types.ts +58 -11
  28. package/index.ts +93 -82
  29. package/package.json +39 -37
  30. package/plugins/index.ts +13 -0
  31. package/scheduler/index.ts +87 -0
  32. package/service/Service.ts +4 -0
  33. package/service/ServiceRegistry.ts +5 -1
  34. package/service/index.ts +1 -1
  35. package/swagger/decorators.ts +65 -0
  36. package/swagger/generator.ts +100 -0
  37. package/swagger/index.ts +2 -0
  38. package/tests/bench/insert.bench.ts +1 -0
  39. package/tests/bench/relations.bench.ts +1 -0
  40. package/tests/bench/sorting.bench.ts +1 -0
  41. package/tests/component-hooks-simple.test.ts +117 -0
  42. package/tests/component-hooks.test.ts +83 -31
  43. package/tests/component.test.ts +1 -0
  44. package/tests/hooks.test.ts +1 -0
  45. package/tests/query.test.ts +46 -4
  46. package/tests/relations.test.ts +1 -0
  47. package/types/app.types.ts +0 -0
  48. package/upload/index.ts +0 -2
  49. package/core/processors/ImageProcessor.ts +0 -423
package/core/App.ts CHANGED
@@ -1,68 +1,87 @@
1
1
  import ApplicationLifecycle, {ApplicationPhase} from "core/ApplicationLifecycle";
2
- import { HasValidBaseTable, PrepareDatabase } from "database/DatabaseHelper";
2
+ import { GenerateTableName, HasValidBaseTable, PrepareDatabase, UpdateComponentIndexes } from "database/DatabaseHelper";
3
3
  import ComponentRegistry from "core/ComponentRegistry";
4
- import { logger } from "core/Logger";
4
+ import { logger as MainLogger } from "core/Logger";
5
+ const logger = MainLogger.child({ scope: "App" });
5
6
  import { createYogaInstance } from "gql";
6
7
  import ServiceRegistry from "service/ServiceRegistry";
7
8
  import type { Plugin } from "graphql-yoga";
8
9
  import * as path from "path";
9
- import { registerDecoratedHooks } from "core/decorators/EntityHooks";
10
10
  import { SchedulerManager } from "core/SchedulerManager";
11
11
  import { registerScheduledTasks } from "core/decorators/ScheduledTask";
12
+ import { OpenAPISpecGenerator, type SwaggerEndpointMetadata } from "swagger";
13
+ import type BasePlugin from "plugins";
12
14
 
13
15
  export default class App {
16
+ private name: string = "BunSane Application";
17
+ private version: string = "1.0.0";
14
18
  private yoga: any;
15
19
  private yogaPlugins: Plugin[] = [];
20
+ private contextFactory?: (context: any) => any;
16
21
  private restEndpoints: Array<{ method: string; path: string; handler: Function; service: any }> = [];
17
22
  private restEndpointMap: Map<string, { method: string; path: string; handler: Function; service: any }> = new Map();
18
23
  private staticAssets: Map<string, string> = new Map();
24
+ private openAPISpecGenerator: OpenAPISpecGenerator | null = null;
25
+ private enforceDocs: boolean = false;
19
26
 
20
- constructor() {
21
- this.init();
27
+ private appReadyCallbacks: Array<() => void> = [];
28
+
29
+ private plugins: BasePlugin[] = [];
30
+
31
+ constructor(appName?: string, appVersion?: string) {
32
+ if (appName) this.name = appName;
33
+ if (appVersion) this.version = appVersion;
34
+ this.openAPISpecGenerator = new OpenAPISpecGenerator(
35
+ this.name,
36
+ this.version,
37
+ );
38
+ return this;
22
39
  }
23
40
 
24
41
  async init() {
25
42
  logger.trace(`Initializing App`);
26
43
  ComponentRegistry.init();
27
44
  ServiceRegistry.init();
28
- if(ApplicationLifecycle.getCurrentPhase() === ApplicationPhase.DATABASE_INITIALIZING) {
29
- if(!await HasValidBaseTable()) {
30
- await PrepareDatabase();
45
+ // Plugin initialization
46
+ for(const plugin of this.plugins) {
47
+ if(plugin.init) {
48
+ await plugin.init(this);
31
49
  }
32
- logger.trace(`Database prepared...`);
33
- ApplicationLifecycle.setPhase(ApplicationPhase.DATABASE_READY);
34
50
  }
35
51
 
36
- ApplicationLifecycle.addPhaseListener((event) => {
52
+ ApplicationLifecycle.addPhaseListener(async (event) => {
37
53
  const phase = event.detail;
38
54
  logger.info(`Application phase changed to: ${phase}`);
55
+ // Notify plugins of phase change
56
+ for(const plugin of this.plugins) {
57
+ if(plugin.onPhaseChange) {
58
+ await plugin.onPhaseChange(phase, this);
59
+ }
60
+ }
39
61
  switch(phase) {
40
62
  case ApplicationPhase.DATABASE_READY: {
41
63
  break;
42
64
  }
43
- case ApplicationPhase.COMPONENTS_READY: {
44
- // Automatically register decorated hooks for all services
45
- const services = ServiceRegistry.getServices();
46
- for (const service of services) {
47
- try {
48
- registerDecoratedHooks(service);
49
- } catch (error) {
50
- logger.warn(`Failed to register hooks for service ${service.constructor.name}`);
51
- logger.warn(error);
52
- }
53
- }
54
- logger.info(`Registered hooks for ${services.length} services`);
55
-
56
- ApplicationLifecycle.setPhase(ApplicationPhase.SYSTEM_REGISTERING);
57
- break;
58
- }
59
65
  case ApplicationPhase.SYSTEM_READY: {
60
66
  try {
61
67
  const schema = ServiceRegistry.getSchema();
68
+
69
+ // Wrap user's context factory to automatically spread Yoga context
70
+ const wrappedContextFactory = this.contextFactory
71
+ ? (yogaContext: any) => {
72
+ const userContext = this.contextFactory!(yogaContext);
73
+ // Merge Yoga's context with user's context, preserving Yoga properties
74
+ return {
75
+ ...yogaContext, // Yoga context (request, params, etc.)
76
+ ...userContext, // User's additional context
77
+ };
78
+ }
79
+ : undefined;
80
+
62
81
  if (schema) {
63
- this.yoga = createYogaInstance(schema, this.yogaPlugins);
82
+ this.yoga = createYogaInstance(schema, this.yogaPlugins, wrappedContextFactory);
64
83
  } else {
65
- this.yoga = createYogaInstance(undefined, this.yogaPlugins);
84
+ this.yoga = createYogaInstance(undefined, this.yogaPlugins, wrappedContextFactory);
66
85
  }
67
86
 
68
87
  // Get all services for processing
@@ -93,12 +112,50 @@ export default class App {
93
112
  handler: endpoint.handler.bind(service),
94
113
  service: service
95
114
  };
115
+ logger.trace(`Registered REST endpoint: [${endpoint.method}] ${endpoint.path} for service ${service.constructor.name}`);
96
116
  this.restEndpoints.push(endpointInfo);
97
117
  this.restEndpointMap.set(`${endpoint.method}:${endpoint.path}`, endpointInfo);
118
+
119
+ // Check if this endpoint has a swagger operation
120
+ if ((endpoint.handler as any).swaggerOperation) {
121
+ // Collect tags from class and method decorators
122
+ const classTags = (service.constructor as any).swaggerClassTags || [];
123
+ const methodTags = (service.constructor as any).swaggerMethodTags?.[endpoint.handler.name] || [];
124
+ const allTags = [...classTags, ...methodTags];
125
+
126
+ logger.trace(`Generating OpenAPI spec for endpoint: [${endpoint.method}] ${endpoint.path} with tags: ${allTags.join(", ")}`);
127
+
128
+ // Merge tags into the operation
129
+ const operation = { ...(endpoint.handler as any).swaggerOperation };
130
+ if (allTags.length > 0) {
131
+ operation.tags = [...(operation.tags || []), ...allTags];
132
+ }
133
+
134
+ this.openAPISpecGenerator!.addEndpoint({
135
+ method: endpoint.method,
136
+ path: endpoint.path,
137
+ operation
138
+ });
139
+ logger.trace(`Registered OpenAPI spec for endpoint: [${endpoint.method}] ${endpoint.path}`);
140
+ } else {
141
+ if(this.enforceDocs) {
142
+ logger.warn(`No swagger operation found for endpoint: [${endpoint.method}] ${endpoint.path} in service ${service.constructor.name}`);
143
+ this.openAPISpecGenerator!.addEndpoint({
144
+ method: endpoint.method,
145
+ path: endpoint.path,
146
+ operation: {
147
+ summary: `No description for ${endpoint.path}. Don't use this endpoint until it's properly documented!`,
148
+ requestBody: {content: {"application/json": {schema: {}}}},
149
+ responses: { "200": { description: "Success" } }
150
+ }
151
+ });
152
+ }
153
+ }
98
154
  }
99
155
  }
100
156
  }
101
157
 
158
+
102
159
  ApplicationLifecycle.setPhase(ApplicationPhase.APPLICATION_READY);
103
160
  } catch (error) {
104
161
  logger.error("Error during SYSTEM_READY phase:");
@@ -114,12 +171,24 @@ export default class App {
114
171
  }
115
172
  }
116
173
  });
174
+
175
+ if(ApplicationLifecycle.getCurrentPhase() === ApplicationPhase.DATABASE_INITIALIZING) {
176
+ if(!await HasValidBaseTable()) {
177
+ await PrepareDatabase();
178
+ }
179
+ logger.trace(`Database prepared...`);
180
+ ApplicationLifecycle.setPhase(ApplicationPhase.DATABASE_READY);
181
+ await ComponentRegistry.registerAllComponents();
182
+ ApplicationLifecycle.setPhase(ApplicationPhase.SYSTEM_REGISTERING);
183
+ }
184
+
185
+
117
186
  }
118
187
 
119
188
  waitForAppReady(): Promise<void> {
120
189
  return new Promise(resolve => {
121
190
  const interval = setInterval(() => {
122
- if (ApplicationLifecycle.getCurrentPhase() === ApplicationPhase.APPLICATION_READY) {
191
+ if (ApplicationLifecycle.getCurrentPhase() >= ApplicationPhase.COMPONENTS_READY) {
123
192
  clearInterval(interval);
124
193
  resolve();
125
194
  }
@@ -127,10 +196,25 @@ export default class App {
127
196
  });
128
197
  }
129
198
 
199
+ public addOpenAPISchema(name: string, schema: any) {
200
+ this.openAPISpecGenerator!.addSchema(name, schema);
201
+ }
202
+ public addOpenAPIServer(url: string, description?: string) {
203
+ this.openAPISpecGenerator!.addServer(url, description);
204
+ }
205
+
130
206
  public addYogaPlugin(plugin: Plugin) {
131
207
  this.yogaPlugins.push(plugin);
132
208
  }
133
209
 
210
+ public setGraphQLContextFactory(factory: (context: any) => any) {
211
+ this.contextFactory = factory;
212
+ }
213
+
214
+ public addPlugin(plugin: BasePlugin) {
215
+ this.plugins.push(plugin);
216
+ }
217
+
134
218
  public addStaticAssets(route: string, folder: string) {
135
219
  // Resolve the folder path relative to the current working directory
136
220
  const resolvedFolder = path.resolve(folder);
@@ -161,6 +245,56 @@ export default class App {
161
245
  headers: { 'Content-Type': 'application/json' }
162
246
  });
163
247
  }
248
+
249
+ // OpenAPI spec endpoint
250
+ if (url.pathname === '/openapi.json') {
251
+ clearTimeout(timeoutId);
252
+ return new Response(this.openAPISpecGenerator!.toJSON(), {
253
+ headers: { 'Content-Type': 'application/json' }
254
+ });
255
+ }
256
+
257
+ // Swagger UI endpoint
258
+ if (url.pathname === '/docs') {
259
+ clearTimeout(timeoutId);
260
+ const swaggerUIHTML = `
261
+ <!DOCTYPE html>
262
+ <html>
263
+ <head>
264
+ <title>${this.name} Documentation</title>
265
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui.css" />
266
+ <style>
267
+ html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
268
+ *, *:before, *:after { box-sizing: inherit; }
269
+ body { margin: 0; background: #fafafa; }
270
+ </style>
271
+ </head>
272
+ <body>
273
+ <div id="swagger-ui"></div>
274
+ <script src="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui-bundle.js"></script>
275
+ <script>
276
+ window.onload = function() {
277
+ const ui = SwaggerUIBundle({
278
+ url: '/openapi.json',
279
+ dom_id: '#swagger-ui',
280
+ deepLinking: true,
281
+ presets: [
282
+ SwaggerUIBundle.presets.apis,
283
+ SwaggerUIBundle.presets.standalone
284
+ ],
285
+ plugins: [
286
+ SwaggerUIBundle.plugins.DownloadUrl
287
+ ],
288
+ layout: "BaseLayout"
289
+ });
290
+ };
291
+ </script>
292
+ </body>
293
+ </html>`;
294
+ return new Response(swaggerUIHTML, {
295
+ headers: { 'Content-Type': 'text/html' }
296
+ });
297
+ }
164
298
  for (const [route, folder] of this.staticAssets) {
165
299
  if (url.pathname.startsWith(route)) {
166
300
  const relativePath = url.pathname.slice(route.length);
@@ -234,12 +368,35 @@ export default class App {
234
368
  }
235
369
  }
236
370
 
371
+ public setName(name: string) {
372
+ this.name = name;
373
+ }
374
+
375
+ public setVersion(version: string) {
376
+ this.version = version;
377
+ }
378
+
379
+ public subscribeAppReady(callback: () => void) {
380
+ this.appReadyCallbacks.push(callback);
381
+ }
382
+
383
+ public enforceSwaggerDocs(value: boolean) {
384
+ this.enforceDocs = value;
385
+ }
386
+
237
387
  async start() {
238
388
  logger.info("Application Started");
389
+ const port = parseInt(process.env.PORT || "3000");
239
390
  const server = Bun.serve({
240
- port: parseInt(process.env.PORT || "3000"),
391
+ port: port,
241
392
  fetch: this.handleRequest.bind(this),
242
393
  });
394
+
395
+ // Update the OpenAPI spec with the actual server URL
396
+ this.openAPISpecGenerator!.addServer(`http://localhost:${port}`, "Development server");
397
+
243
398
  logger.info(`Server is running on ${new URL(this.yoga?.graphqlEndpoint || '/graphql', `http://${server.hostname}:${server.port}`)}`)
399
+
400
+ this.appReadyCallbacks.forEach(cb => cb());
244
401
  }
245
402
  }