bunsane 0.1.2 → 0.1.3
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/TODO.md +1 -1
- package/bun.lock +156 -150
- package/core/App.ts +188 -31
- package/core/ArcheType.ts +1044 -26
- package/core/ComponentRegistry.ts +172 -29
- package/core/Components.ts +102 -24
- package/core/Decorators.ts +0 -1
- package/core/Entity.ts +55 -7
- package/core/EntityInterface.ts +4 -0
- package/core/EntityManager.ts +4 -4
- package/core/Query.ts +169 -3
- package/core/RequestLoaders.ts +101 -12
- package/core/SchedulerManager.ts +3 -4
- package/core/metadata/definitions/ArcheType.ts +9 -0
- package/core/metadata/definitions/Component.ts +16 -0
- package/core/metadata/definitions/gqlObject.ts +10 -0
- package/core/metadata/getMetadataStorage.ts +14 -0
- package/core/metadata/index.ts +17 -0
- package/core/metadata/metadata-storage.ts +81 -0
- package/database/DatabaseHelper.ts +22 -20
- package/database/sqlHelpers.ts +0 -2
- package/gql/ArchetypeOperations.ts +281 -0
- package/gql/Generator.ts +252 -62
- package/gql/helpers.ts +5 -5
- package/gql/index.ts +19 -17
- package/gql/types.ts +58 -11
- package/index.ts +93 -82
- package/package.json +39 -37
- package/plugins/index.ts +13 -0
- package/scheduler/index.ts +87 -0
- package/service/Service.ts +4 -0
- package/service/ServiceRegistry.ts +5 -1
- package/service/index.ts +1 -1
- package/swagger/decorators.ts +65 -0
- package/swagger/generator.ts +100 -0
- package/swagger/index.ts +2 -0
- package/tests/bench/insert.bench.ts +1 -0
- package/tests/bench/relations.bench.ts +1 -0
- package/tests/bench/sorting.bench.ts +1 -0
- package/tests/component-hooks-simple.test.ts +117 -0
- package/tests/component-hooks.test.ts +83 -31
- package/tests/component.test.ts +1 -0
- package/tests/hooks.test.ts +1 -0
- package/tests/query.test.ts +46 -4
- package/tests/relations.test.ts +1 -0
- package/types/app.types.ts +0 -0
- package/upload/index.ts +0 -2
- 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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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()
|
|
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:
|
|
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
|
}
|