bunsane 0.1.4 → 0.2.0
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/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
package/core/App.ts
CHANGED
|
@@ -1,16 +1,47 @@
|
|
|
1
|
-
import ApplicationLifecycle, {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
1
|
+
import ApplicationLifecycle, {
|
|
2
|
+
ApplicationPhase,
|
|
3
|
+
} from "./ApplicationLifecycle";
|
|
4
|
+
import {
|
|
5
|
+
GenerateTableName,
|
|
6
|
+
HasValidBaseTable,
|
|
7
|
+
PrepareDatabase,
|
|
8
|
+
UpdateComponentIndexes,
|
|
9
|
+
EnsureDatabaseMigrations,
|
|
10
|
+
} from "../database/DatabaseHelper";
|
|
11
|
+
import { ComponentRegistry } from "./components";
|
|
12
|
+
import { logger as MainLogger } from "./Logger";
|
|
13
|
+
import { getSerializedMetadataStorage } from "./metadata";
|
|
5
14
|
const logger = MainLogger.child({ scope: "App" });
|
|
6
|
-
import { createYogaInstance } from "gql";
|
|
7
|
-
import ServiceRegistry from "service/ServiceRegistry";
|
|
8
|
-
import type
|
|
15
|
+
import { createYogaInstance } from "../gql";
|
|
16
|
+
import ServiceRegistry from "../service/ServiceRegistry";
|
|
17
|
+
import { type Plugin, createPubSub } from "graphql-yoga";
|
|
9
18
|
import * as path from "path";
|
|
10
|
-
import { SchedulerManager } from "
|
|
11
|
-
import { registerScheduledTasks } from "
|
|
12
|
-
import { OpenAPISpecGenerator, type SwaggerEndpointMetadata } from "swagger";
|
|
13
|
-
import type BasePlugin from "plugins";
|
|
19
|
+
import { SchedulerManager } from "./SchedulerManager";
|
|
20
|
+
import { registerScheduledTasks } from "../scheduler";
|
|
21
|
+
import { OpenAPISpecGenerator, type SwaggerEndpointMetadata } from "../swagger";
|
|
22
|
+
import type BasePlugin from "../plugins";
|
|
23
|
+
import { preparedStatementCache } from "../database/PreparedStatementCache";
|
|
24
|
+
import db from "../database";
|
|
25
|
+
import studioEndpoint from "../endpoints";
|
|
26
|
+
import { type Middleware, composeMiddleware } from "./Middleware";
|
|
27
|
+
import { deepHealthCheck, readinessCheck } from "./health";
|
|
28
|
+
import { validateEnv } from "./validateEnv";
|
|
29
|
+
|
|
30
|
+
export type CorsConfig = {
|
|
31
|
+
origin?: string | string[] | ((origin: string) => boolean);
|
|
32
|
+
credentials?: boolean;
|
|
33
|
+
allowedHeaders?: string[];
|
|
34
|
+
exposedHeaders?: string[];
|
|
35
|
+
methods?: string[];
|
|
36
|
+
maxAge?: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type AppConfig = {
|
|
40
|
+
scheduler: {
|
|
41
|
+
logging: boolean;
|
|
42
|
+
};
|
|
43
|
+
cors?: CorsConfig;
|
|
44
|
+
};
|
|
14
45
|
|
|
15
46
|
export default class App {
|
|
16
47
|
private name: string = "BunSane Application";
|
|
@@ -18,8 +49,16 @@ export default class App {
|
|
|
18
49
|
private yoga: any;
|
|
19
50
|
private yogaPlugins: Plugin[] = [];
|
|
20
51
|
private contextFactory?: (context: any) => any;
|
|
21
|
-
private restEndpoints: Array<{
|
|
22
|
-
|
|
52
|
+
private restEndpoints: Array<{
|
|
53
|
+
method: string;
|
|
54
|
+
path: string;
|
|
55
|
+
handler: Function;
|
|
56
|
+
service: any;
|
|
57
|
+
}> = [];
|
|
58
|
+
private restEndpointMap: Map<
|
|
59
|
+
string,
|
|
60
|
+
{ method: string; path: string; handler: Function; service: any }
|
|
61
|
+
> = new Map();
|
|
23
62
|
private staticAssets: Map<string, string> = new Map();
|
|
24
63
|
private openAPISpecGenerator: OpenAPISpecGenerator | null = null;
|
|
25
64
|
private enforceDocs: boolean = false;
|
|
@@ -27,24 +66,83 @@ export default class App {
|
|
|
27
66
|
private appReadyCallbacks: Array<() => void> = [];
|
|
28
67
|
|
|
29
68
|
private plugins: BasePlugin[] = [];
|
|
69
|
+
private middlewares: Middleware[] = [];
|
|
70
|
+
private composedHandler: ((req: Request) => Promise<Response>) | null = null;
|
|
71
|
+
|
|
72
|
+
private studioEnabled: boolean = false;
|
|
73
|
+
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
74
|
+
private isShuttingDown = false;
|
|
75
|
+
private isReady = false;
|
|
76
|
+
private graphqlMaxDepth: number = 10;
|
|
77
|
+
private shutdownGracePeriod = 10_000;
|
|
78
|
+
private maxRequestBodySize = 50 * 1024 * 1024; // 50MB default
|
|
79
|
+
|
|
80
|
+
pubSub = createPubSub();
|
|
81
|
+
|
|
82
|
+
public config: AppConfig = {
|
|
83
|
+
scheduler: {
|
|
84
|
+
logging: false,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
30
87
|
|
|
31
88
|
constructor(appName?: string, appVersion?: string) {
|
|
32
89
|
if (appName) this.name = appName;
|
|
33
90
|
if (appVersion) this.version = appVersion;
|
|
34
91
|
this.openAPISpecGenerator = new OpenAPISpecGenerator(
|
|
35
92
|
this.name,
|
|
36
|
-
this.version
|
|
93
|
+
this.version
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Automatically serve the studio if it exists
|
|
97
|
+
const studioPath = path.join(
|
|
98
|
+
import.meta.dirname,
|
|
99
|
+
"..",
|
|
100
|
+
"studio",
|
|
101
|
+
"dist"
|
|
37
102
|
);
|
|
103
|
+
try {
|
|
104
|
+
const studioDir = Bun.file(studioPath);
|
|
105
|
+
if (studioDir) {
|
|
106
|
+
this.addStaticAssets("/studio", studioPath);
|
|
107
|
+
logger.info("Studio assets loaded from:" + studioPath);
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.warn(
|
|
111
|
+
"Studio not found, skipping studio setup:",
|
|
112
|
+
error as any
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
38
116
|
return this;
|
|
39
117
|
}
|
|
40
118
|
|
|
119
|
+
public setCors(cors: CorsConfig) {
|
|
120
|
+
this.config.cors = cors;
|
|
121
|
+
// Warn about invalid configuration
|
|
122
|
+
if (cors.credentials && cors.origin === '*') {
|
|
123
|
+
console.warn('[CORS] Warning: credentials=true with origin="*" is invalid per spec. Origin will be reflected from request.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
41
127
|
async init() {
|
|
128
|
+
validateEnv();
|
|
42
129
|
logger.trace(`Initializing App`);
|
|
43
130
|
ComponentRegistry.init();
|
|
44
131
|
ServiceRegistry.init();
|
|
132
|
+
|
|
133
|
+
// Initialize CacheManager
|
|
134
|
+
try {
|
|
135
|
+
const { CacheManager } = await import('./cache/CacheManager');
|
|
136
|
+
const cacheManager = CacheManager.getInstance();
|
|
137
|
+
// CacheManager initializes with default config, can be customized later
|
|
138
|
+
logger.info({ scope: 'cache', component: 'App', msg: 'CacheManager initialized' });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.warn({ scope: 'cache', component: 'App', msg: 'Failed to initialize CacheManager', error });
|
|
141
|
+
}
|
|
142
|
+
|
|
45
143
|
// Plugin initialization
|
|
46
|
-
for(const plugin of this.plugins) {
|
|
47
|
-
if(plugin.init) {
|
|
144
|
+
for (const plugin of this.plugins) {
|
|
145
|
+
if (plugin.init) {
|
|
48
146
|
await plugin.init(this);
|
|
49
147
|
}
|
|
50
148
|
}
|
|
@@ -53,35 +151,84 @@ export default class App {
|
|
|
53
151
|
const phase = event.detail;
|
|
54
152
|
logger.info(`Application phase changed to: ${phase}`);
|
|
55
153
|
// Notify plugins of phase change
|
|
56
|
-
for(const plugin of this.plugins) {
|
|
57
|
-
if(plugin.onPhaseChange) {
|
|
154
|
+
for (const plugin of this.plugins) {
|
|
155
|
+
if (plugin.onPhaseChange) {
|
|
58
156
|
await plugin.onPhaseChange(phase, this);
|
|
59
157
|
}
|
|
60
158
|
}
|
|
61
|
-
switch(phase) {
|
|
159
|
+
switch (phase) {
|
|
62
160
|
case ApplicationPhase.DATABASE_READY: {
|
|
161
|
+
// Warm up prepared statement cache with common query patterns
|
|
162
|
+
try {
|
|
163
|
+
await this.warmUpPreparedStatementCache();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
logger.warn(
|
|
166
|
+
"Failed to warm up prepared statement cache:",
|
|
167
|
+
error as any
|
|
168
|
+
);
|
|
169
|
+
}
|
|
63
170
|
break;
|
|
64
171
|
}
|
|
65
172
|
case ApplicationPhase.SYSTEM_READY: {
|
|
173
|
+
// Perform cache health check
|
|
66
174
|
try {
|
|
67
|
-
const
|
|
175
|
+
const { CacheManager } = await import('./cache/CacheManager');
|
|
176
|
+
const cacheManager = CacheManager.getInstance();
|
|
177
|
+
const config = cacheManager.getConfig();
|
|
68
178
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
...yogaContext, // Yoga context (request, params, etc.)
|
|
76
|
-
...userContext, // User's additional context
|
|
77
|
-
};
|
|
179
|
+
if (config.enabled) {
|
|
180
|
+
const isHealthy = await cacheManager.getProvider().ping();
|
|
181
|
+
if (isHealthy) {
|
|
182
|
+
logger.info({ scope: 'cache', component: 'App', msg: 'Cache health check passed' });
|
|
183
|
+
} else {
|
|
184
|
+
logger.warn({ scope: 'cache', component: 'App', msg: 'Cache health check failed' });
|
|
78
185
|
}
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
logger.warn({ scope: 'cache', component: 'App', msg: 'Cache health check error', error });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const schema = ServiceRegistry.getSchema();
|
|
193
|
+
|
|
194
|
+
// Wrap user's context factory to automatically spread Yoga context
|
|
195
|
+
const wrappedContextFactory = this.contextFactory
|
|
196
|
+
? async (yogaContext: any) => {
|
|
197
|
+
const userContext =
|
|
198
|
+
await this.contextFactory!(yogaContext);
|
|
199
|
+
// Merge Yoga's context with user's context, preserving Yoga properties
|
|
200
|
+
return {
|
|
201
|
+
...yogaContext, // Yoga context (request, params, etc.)
|
|
202
|
+
...userContext, // User's additional context
|
|
203
|
+
};
|
|
204
|
+
}
|
|
79
205
|
: undefined;
|
|
80
|
-
|
|
206
|
+
|
|
207
|
+
// Read env override for GraphQL depth limit
|
|
208
|
+
const envDepth = process.env.GRAPHQL_MAX_DEPTH;
|
|
209
|
+
if (envDepth) {
|
|
210
|
+
this.graphqlMaxDepth = parseInt(envDepth, 10);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const yogaOptions = {
|
|
214
|
+
cors: this.config.cors,
|
|
215
|
+
maxDepth: this.graphqlMaxDepth || undefined,
|
|
216
|
+
};
|
|
217
|
+
|
|
81
218
|
if (schema) {
|
|
82
|
-
this.yoga = createYogaInstance(
|
|
219
|
+
this.yoga = createYogaInstance(
|
|
220
|
+
schema,
|
|
221
|
+
this.yogaPlugins,
|
|
222
|
+
wrappedContextFactory,
|
|
223
|
+
yogaOptions
|
|
224
|
+
);
|
|
83
225
|
} else {
|
|
84
|
-
this.yoga = createYogaInstance(
|
|
226
|
+
this.yoga = createYogaInstance(
|
|
227
|
+
undefined,
|
|
228
|
+
this.yogaPlugins,
|
|
229
|
+
wrappedContextFactory,
|
|
230
|
+
yogaOptions
|
|
231
|
+
);
|
|
85
232
|
}
|
|
86
233
|
|
|
87
234
|
// Get all services for processing
|
|
@@ -89,74 +236,129 @@ export default class App {
|
|
|
89
236
|
|
|
90
237
|
// Initialize Scheduler
|
|
91
238
|
const scheduler = SchedulerManager.getInstance();
|
|
239
|
+
scheduler.config.enableLogging =
|
|
240
|
+
this.config.scheduler.logging;
|
|
92
241
|
|
|
93
242
|
// Register scheduled tasks for all services
|
|
94
243
|
for (const service of services) {
|
|
95
244
|
try {
|
|
96
245
|
registerScheduledTasks(service);
|
|
97
246
|
} catch (error) {
|
|
98
|
-
logger.warn(
|
|
247
|
+
logger.warn(
|
|
248
|
+
`Failed to register scheduled tasks for service ${service.constructor.name}`
|
|
249
|
+
);
|
|
99
250
|
logger.warn(error);
|
|
100
251
|
}
|
|
101
252
|
}
|
|
102
|
-
logger.info(
|
|
253
|
+
logger.info(
|
|
254
|
+
`Registered scheduled tasks for ${services.length} services`
|
|
255
|
+
);
|
|
103
256
|
|
|
104
257
|
// Collect REST endpoints from all services
|
|
105
258
|
for (const service of services) {
|
|
106
|
-
const endpoints = (service.constructor as any)
|
|
259
|
+
const endpoints = (service.constructor as any)
|
|
260
|
+
.httpEndpoints;
|
|
107
261
|
if (endpoints) {
|
|
108
262
|
for (const endpoint of endpoints) {
|
|
109
263
|
const endpointInfo = {
|
|
110
264
|
method: endpoint.method,
|
|
111
265
|
path: endpoint.path,
|
|
112
266
|
handler: endpoint.handler.bind(service),
|
|
113
|
-
service: service
|
|
267
|
+
service: service,
|
|
114
268
|
};
|
|
115
|
-
logger.trace(
|
|
269
|
+
logger.trace(
|
|
270
|
+
`Registered REST endpoint: [${endpoint.method}] ${endpoint.path} for service ${service.constructor.name}`
|
|
271
|
+
);
|
|
116
272
|
this.restEndpoints.push(endpointInfo);
|
|
117
|
-
this.restEndpointMap.set(
|
|
273
|
+
this.restEndpointMap.set(
|
|
274
|
+
`${endpoint.method}:${endpoint.path}`,
|
|
275
|
+
endpointInfo
|
|
276
|
+
);
|
|
118
277
|
|
|
119
278
|
// Check if this endpoint has a swagger operation
|
|
120
|
-
if (
|
|
279
|
+
if (
|
|
280
|
+
(endpoint.handler as any)
|
|
281
|
+
.swaggerOperation
|
|
282
|
+
) {
|
|
121
283
|
// Collect tags from class and method decorators
|
|
122
|
-
const classTags =
|
|
123
|
-
|
|
124
|
-
|
|
284
|
+
const classTags =
|
|
285
|
+
(service.constructor as any)
|
|
286
|
+
.swaggerClassTags || [];
|
|
287
|
+
const methodTags =
|
|
288
|
+
(service.constructor as any)
|
|
289
|
+
.swaggerMethodTags?.[
|
|
290
|
+
endpoint.handler.name
|
|
291
|
+
] || [];
|
|
292
|
+
const allTags = [
|
|
293
|
+
...classTags,
|
|
294
|
+
...methodTags,
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
logger.trace(
|
|
298
|
+
`Generating OpenAPI spec for endpoint: [${
|
|
299
|
+
endpoint.method
|
|
300
|
+
}] ${
|
|
301
|
+
endpoint.path
|
|
302
|
+
} with tags: ${allTags.join(", ")}`
|
|
303
|
+
);
|
|
125
304
|
|
|
126
|
-
logger.trace(`Generating OpenAPI spec for endpoint: [${endpoint.method}] ${endpoint.path} with tags: ${allTags.join(", ")}`);
|
|
127
|
-
|
|
128
305
|
// Merge tags into the operation
|
|
129
|
-
const operation = {
|
|
306
|
+
const operation = {
|
|
307
|
+
...(endpoint.handler as any)
|
|
308
|
+
.swaggerOperation,
|
|
309
|
+
};
|
|
130
310
|
if (allTags.length > 0) {
|
|
131
|
-
operation.tags = [
|
|
132
|
-
|
|
133
|
-
|
|
311
|
+
operation.tags = [
|
|
312
|
+
...(operation.tags || []),
|
|
313
|
+
...allTags,
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
|
|
134
317
|
this.openAPISpecGenerator!.addEndpoint({
|
|
135
318
|
method: endpoint.method,
|
|
136
319
|
path: endpoint.path,
|
|
137
|
-
operation
|
|
320
|
+
operation,
|
|
138
321
|
});
|
|
139
|
-
logger.trace(
|
|
322
|
+
logger.trace(
|
|
323
|
+
`Registered OpenAPI spec for endpoint: [${endpoint.method}] ${endpoint.path}`
|
|
324
|
+
);
|
|
140
325
|
} else {
|
|
141
|
-
if(this.enforceDocs) {
|
|
142
|
-
logger.warn(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
326
|
+
if (this.enforceDocs) {
|
|
327
|
+
logger.warn(
|
|
328
|
+
`No swagger operation found for endpoint: [${endpoint.method}] ${endpoint.path} in service ${service.constructor.name}`
|
|
329
|
+
);
|
|
330
|
+
this.openAPISpecGenerator!.addEndpoint(
|
|
331
|
+
{
|
|
332
|
+
method: endpoint.method,
|
|
333
|
+
path: endpoint.path,
|
|
334
|
+
operation: {
|
|
335
|
+
summary: `No description for ${endpoint.path}. Don't use this endpoint until it's properly documented!`,
|
|
336
|
+
requestBody: {
|
|
337
|
+
content: {
|
|
338
|
+
"application/json":
|
|
339
|
+
{
|
|
340
|
+
schema: {},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
responses: {
|
|
345
|
+
"200": {
|
|
346
|
+
description:
|
|
347
|
+
"Success",
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
150
351
|
}
|
|
151
|
-
|
|
352
|
+
);
|
|
152
353
|
}
|
|
153
354
|
}
|
|
154
355
|
}
|
|
155
356
|
}
|
|
156
357
|
}
|
|
157
358
|
|
|
158
|
-
|
|
159
|
-
|
|
359
|
+
ApplicationLifecycle.setPhase(
|
|
360
|
+
ApplicationPhase.APPLICATION_READY
|
|
361
|
+
);
|
|
160
362
|
} catch (error) {
|
|
161
363
|
logger.error("Error during SYSTEM_READY phase:");
|
|
162
364
|
logger.error(error);
|
|
@@ -164,7 +366,7 @@ export default class App {
|
|
|
164
366
|
break;
|
|
165
367
|
}
|
|
166
368
|
case ApplicationPhase.APPLICATION_READY: {
|
|
167
|
-
if(process.env.NODE_ENV !== "test") {
|
|
369
|
+
if (process.env.NODE_ENV !== "test") {
|
|
168
370
|
this.start();
|
|
169
371
|
}
|
|
170
372
|
break;
|
|
@@ -172,23 +374,30 @@ export default class App {
|
|
|
172
374
|
}
|
|
173
375
|
});
|
|
174
376
|
|
|
175
|
-
if(
|
|
176
|
-
|
|
377
|
+
if (
|
|
378
|
+
ApplicationLifecycle.getCurrentPhase() ===
|
|
379
|
+
ApplicationPhase.DATABASE_INITIALIZING
|
|
380
|
+
) {
|
|
381
|
+
if (!(await HasValidBaseTable())) {
|
|
177
382
|
await PrepareDatabase();
|
|
383
|
+
} else {
|
|
384
|
+
// Check for missing columns and run migrations
|
|
385
|
+
await EnsureDatabaseMigrations();
|
|
178
386
|
}
|
|
179
387
|
logger.trace(`Database prepared...`);
|
|
180
388
|
ApplicationLifecycle.setPhase(ApplicationPhase.DATABASE_READY);
|
|
181
389
|
await ComponentRegistry.registerAllComponents();
|
|
182
390
|
ApplicationLifecycle.setPhase(ApplicationPhase.SYSTEM_REGISTERING);
|
|
183
391
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
392
|
}
|
|
187
393
|
|
|
188
394
|
waitForAppReady(): Promise<void> {
|
|
189
|
-
return new Promise(resolve => {
|
|
395
|
+
return new Promise((resolve) => {
|
|
190
396
|
const interval = setInterval(() => {
|
|
191
|
-
if (
|
|
397
|
+
if (
|
|
398
|
+
ApplicationLifecycle.getCurrentPhase() >=
|
|
399
|
+
ApplicationPhase.APPLICATION_READY
|
|
400
|
+
) {
|
|
192
401
|
clearInterval(interval);
|
|
193
402
|
resolve();
|
|
194
403
|
}
|
|
@@ -213,7 +422,15 @@ export default class App {
|
|
|
213
422
|
|
|
214
423
|
public addPlugin(plugin: BasePlugin) {
|
|
215
424
|
this.plugins.push(plugin);
|
|
216
|
-
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Register an HTTP middleware. Middlewares execute in registration order,
|
|
429
|
+
* wrapping around the core request handler (onion model).
|
|
430
|
+
*/
|
|
431
|
+
public use(middleware: Middleware) {
|
|
432
|
+
this.middlewares.push(middleware);
|
|
433
|
+
}
|
|
217
434
|
|
|
218
435
|
public addStaticAssets(route: string, folder: string) {
|
|
219
436
|
// Resolve the folder path relative to the current working directory
|
|
@@ -221,11 +438,95 @@ export default class App {
|
|
|
221
438
|
this.staticAssets.set(route, resolvedFolder);
|
|
222
439
|
}
|
|
223
440
|
|
|
441
|
+
private validateOrigin(requestOrigin: string | null | undefined): string | null {
|
|
442
|
+
if (!this.config.cors || !requestOrigin) return null;
|
|
443
|
+
|
|
444
|
+
const configOrigin = this.config.cors.origin;
|
|
445
|
+
|
|
446
|
+
// Wildcard allows all
|
|
447
|
+
if (configOrigin === '*' || configOrigin === undefined) {
|
|
448
|
+
// If credentials enabled, cannot use wildcard - return actual origin
|
|
449
|
+
return this.config.cors.credentials ? requestOrigin : '*';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// String match
|
|
453
|
+
if (typeof configOrigin === 'string') {
|
|
454
|
+
return requestOrigin === configOrigin ? configOrigin : null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Array - check if origin is in list
|
|
458
|
+
if (Array.isArray(configOrigin)) {
|
|
459
|
+
return configOrigin.includes(requestOrigin) ? requestOrigin : null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Function validator
|
|
463
|
+
if (typeof configOrigin === 'function') {
|
|
464
|
+
return configOrigin(requestOrigin) ? requestOrigin : null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private getCorsHeaders(req?: Request): Record<string, string> {
|
|
471
|
+
if (!this.config.cors) return {};
|
|
472
|
+
|
|
473
|
+
const requestOrigin = req?.headers.get('Origin');
|
|
474
|
+
const allowedOrigin = this.validateOrigin(requestOrigin);
|
|
475
|
+
|
|
476
|
+
// If origin not allowed, return empty (no CORS headers)
|
|
477
|
+
if (requestOrigin && !allowedOrigin) return {};
|
|
478
|
+
|
|
479
|
+
const headers: Record<string, string> = {
|
|
480
|
+
'Access-Control-Allow-Origin': allowedOrigin || '*',
|
|
481
|
+
'Access-Control-Allow-Methods': this.config.cors.methods?.join(', ') || 'GET, POST, PUT, DELETE, OPTIONS',
|
|
482
|
+
'Access-Control-Allow-Headers': this.config.cors.allowedHeaders?.join(', ') || 'Content-Type, Authorization',
|
|
483
|
+
'Vary': 'Origin',
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
if (this.config.cors.credentials) {
|
|
487
|
+
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (this.config.cors.exposedHeaders?.length) {
|
|
491
|
+
headers['Access-Control-Expose-Headers'] = this.config.cors.exposedHeaders.join(', ');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (this.config.cors.maxAge !== undefined) {
|
|
495
|
+
headers['Access-Control-Max-Age'] = String(this.config.cors.maxAge);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return headers;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private addCorsHeaders(response: Response, req?: Request): Response {
|
|
502
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
503
|
+
if (Object.keys(corsHeaders).length === 0) return response;
|
|
504
|
+
|
|
505
|
+
const newHeaders = new Headers(response.headers);
|
|
506
|
+
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
507
|
+
newHeaders.set(key, value);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return new Response(response.body, {
|
|
511
|
+
status: response.status,
|
|
512
|
+
statusText: response.statusText,
|
|
513
|
+
headers: newHeaders,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
224
517
|
private async handleRequest(req: Request): Promise<Response> {
|
|
225
518
|
const url = new URL(req.url);
|
|
226
519
|
const method = req.method;
|
|
227
520
|
const startTime = Date.now();
|
|
228
|
-
|
|
521
|
+
|
|
522
|
+
// Handle CORS preflight requests
|
|
523
|
+
if (method === 'OPTIONS') {
|
|
524
|
+
return new Response(null, {
|
|
525
|
+
status: 204,
|
|
526
|
+
headers: this.getCorsHeaders(req),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
229
530
|
// Add request timeout
|
|
230
531
|
const controller = new AbortController();
|
|
231
532
|
const timeoutId = setTimeout(() => {
|
|
@@ -235,27 +536,54 @@ export default class App {
|
|
|
235
536
|
|
|
236
537
|
try {
|
|
237
538
|
// Health check endpoint
|
|
238
|
-
if (url.pathname ===
|
|
539
|
+
if (url.pathname === "/health") {
|
|
239
540
|
clearTimeout(timeoutId);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
541
|
+
const health = await deepHealthCheck();
|
|
542
|
+
return this.addCorsHeaders(new Response(
|
|
543
|
+
JSON.stringify(health.result),
|
|
544
|
+
{
|
|
545
|
+
status: health.httpStatus,
|
|
546
|
+
headers: { "Content-Type": "application/json" },
|
|
547
|
+
}
|
|
548
|
+
), req);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Metrics endpoint
|
|
552
|
+
if (url.pathname === "/metrics") {
|
|
553
|
+
clearTimeout(timeoutId);
|
|
554
|
+
const metrics = await this.collectMetrics();
|
|
555
|
+
return this.addCorsHeaders(new Response(
|
|
556
|
+
JSON.stringify(metrics),
|
|
557
|
+
{
|
|
558
|
+
status: 200,
|
|
559
|
+
headers: { "Content-Type": "application/json" },
|
|
560
|
+
}
|
|
561
|
+
), req);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Readiness probe
|
|
565
|
+
if (url.pathname === "/health/ready") {
|
|
566
|
+
clearTimeout(timeoutId);
|
|
567
|
+
const ready = await readinessCheck(this.isReady, this.isShuttingDown);
|
|
568
|
+
return this.addCorsHeaders(new Response(
|
|
569
|
+
JSON.stringify(ready.result),
|
|
570
|
+
{
|
|
571
|
+
status: ready.httpStatus,
|
|
572
|
+
headers: { "Content-Type": "application/json" },
|
|
573
|
+
}
|
|
574
|
+
), req);
|
|
247
575
|
}
|
|
248
576
|
|
|
249
577
|
// OpenAPI spec endpoint
|
|
250
|
-
if (url.pathname ===
|
|
578
|
+
if (url.pathname === "/openapi.json") {
|
|
251
579
|
clearTimeout(timeoutId);
|
|
252
|
-
return new Response(this.openAPISpecGenerator!.toJSON(), {
|
|
253
|
-
headers: {
|
|
254
|
-
});
|
|
580
|
+
return this.addCorsHeaders(new Response(this.openAPISpecGenerator!.toJSON(), {
|
|
581
|
+
headers: { "Content-Type": "application/json" },
|
|
582
|
+
}), req);
|
|
255
583
|
}
|
|
256
584
|
|
|
257
585
|
// Swagger UI endpoint
|
|
258
|
-
if (url.pathname ===
|
|
586
|
+
if (url.pathname === "/docs") {
|
|
259
587
|
clearTimeout(timeoutId);
|
|
260
588
|
const swaggerUIHTML = `
|
|
261
589
|
<!DOCTYPE html>
|
|
@@ -291,9 +619,173 @@ export default class App {
|
|
|
291
619
|
</script>
|
|
292
620
|
</body>
|
|
293
621
|
</html>`;
|
|
294
|
-
return new Response(swaggerUIHTML, {
|
|
295
|
-
headers: {
|
|
296
|
-
});
|
|
622
|
+
return this.addCorsHeaders(new Response(swaggerUIHTML, {
|
|
623
|
+
headers: { "Content-Type": "text/html" },
|
|
624
|
+
}), req);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Studio API endpoints
|
|
628
|
+
if (this.studioEnabled && url.pathname.startsWith("/studio/api/")) {
|
|
629
|
+
clearTimeout(timeoutId);
|
|
630
|
+
|
|
631
|
+
// Studio tables endpoint
|
|
632
|
+
if (url.pathname === "/studio/api/tables") {
|
|
633
|
+
return this.addCorsHeaders(await studioEndpoint.getTables(), req);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Studio stats endpoint
|
|
637
|
+
if (url.pathname === "/studio/api/stats") {
|
|
638
|
+
return this.addCorsHeaders(await studioEndpoint.handleStudioStatsRequest(), req);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Studio components endpoint
|
|
642
|
+
if (url.pathname === "/studio/api/components") {
|
|
643
|
+
return this.addCorsHeaders(await studioEndpoint.handleStudioComponentsRequest(), req);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Studio query endpoint (POST only)
|
|
647
|
+
if (url.pathname === "/studio/api/query" && method === "POST") {
|
|
648
|
+
const body = await req.json();
|
|
649
|
+
return this.addCorsHeaders(await studioEndpoint.handleStudioQueryRequest(body), req);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const studioApiPath = url.pathname.replace("/studio/api/", "");
|
|
653
|
+
const pathSegments = studioApiPath.split("/");
|
|
654
|
+
|
|
655
|
+
if (pathSegments[0] === "entity" && pathSegments[1]) {
|
|
656
|
+
const entityId = pathSegments[1];
|
|
657
|
+
return this.addCorsHeaders(
|
|
658
|
+
await studioEndpoint.handleEntityInspectorRequest(entityId),
|
|
659
|
+
req
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (pathSegments[0] === "table" && pathSegments[1]) {
|
|
664
|
+
const tableName = pathSegments[1];
|
|
665
|
+
|
|
666
|
+
if (method === "DELETE") {
|
|
667
|
+
const body = await req.json();
|
|
668
|
+
return this.addCorsHeaders(await studioEndpoint.handleStudioTableDeleteRequest(
|
|
669
|
+
tableName,
|
|
670
|
+
body
|
|
671
|
+
), req);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const limit = url.searchParams.get("limit");
|
|
675
|
+
const offset = url.searchParams.get("offset");
|
|
676
|
+
const search = url.searchParams.get("search");
|
|
677
|
+
|
|
678
|
+
return this.addCorsHeaders(await studioEndpoint.handleStudioTableRequest(tableName, {
|
|
679
|
+
limit: limit ? parseInt(limit, 10) : undefined,
|
|
680
|
+
offset: offset ? parseInt(offset, 10) : undefined,
|
|
681
|
+
search: search ?? undefined,
|
|
682
|
+
}), req);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (pathSegments[0] === "arche-type" && pathSegments[1]) {
|
|
686
|
+
const archeTypeName = pathSegments[1];
|
|
687
|
+
|
|
688
|
+
if (method === "DELETE") {
|
|
689
|
+
const body = await req.json();
|
|
690
|
+
return this.addCorsHeaders(await studioEndpoint.handleStudioArcheTypeDeleteRequest(
|
|
691
|
+
archeTypeName,
|
|
692
|
+
body
|
|
693
|
+
), req);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const limit = url.searchParams.get("limit");
|
|
697
|
+
const offset = url.searchParams.get("offset");
|
|
698
|
+
const search = url.searchParams.get("search");
|
|
699
|
+
const includeDeleted = url.searchParams.get("include_deleted");
|
|
700
|
+
|
|
701
|
+
return this.addCorsHeaders(await studioEndpoint.handleStudioArcheTypeRecordsRequest(
|
|
702
|
+
archeTypeName,
|
|
703
|
+
{
|
|
704
|
+
limit: limit ? parseInt(limit, 10) : undefined,
|
|
705
|
+
offset: offset ? parseInt(offset, 10) : undefined,
|
|
706
|
+
search: search ?? undefined,
|
|
707
|
+
include_deleted: includeDeleted === "true",
|
|
708
|
+
}
|
|
709
|
+
), req);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return this.addCorsHeaders(new Response(
|
|
713
|
+
JSON.stringify({ error: "Studio API endpoint not found" }),
|
|
714
|
+
{
|
|
715
|
+
status: 404,
|
|
716
|
+
headers: { "Content-Type": "application/json" },
|
|
717
|
+
}
|
|
718
|
+
), req);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Studio endpoint - handle both root and all sub-routes
|
|
722
|
+
if (
|
|
723
|
+
this.studioEnabled &&
|
|
724
|
+
(url.pathname === "/studio" ||
|
|
725
|
+
url.pathname.startsWith("/studio/"))
|
|
726
|
+
) {
|
|
727
|
+
clearTimeout(timeoutId);
|
|
728
|
+
|
|
729
|
+
// Skip API routes - they're handled by the API handler above
|
|
730
|
+
if (url.pathname.startsWith("/studio/api/")) {
|
|
731
|
+
return this.addCorsHeaders(new Response(
|
|
732
|
+
JSON.stringify({
|
|
733
|
+
error: "Studio API endpoint not found",
|
|
734
|
+
}),
|
|
735
|
+
{
|
|
736
|
+
status: 404,
|
|
737
|
+
headers: { "Content-Type": "application/json" },
|
|
738
|
+
}
|
|
739
|
+
), req);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Check if this is a request for static assets (CSS, JS, etc.)
|
|
743
|
+
if (url.pathname.startsWith("/studio/assets/")) {
|
|
744
|
+
// Let the static assets handler below handle this
|
|
745
|
+
// Don't return here, fall through to static assets handler
|
|
746
|
+
} else {
|
|
747
|
+
// For all other /studio/* routes, serve the React app's index.html
|
|
748
|
+
const studioIndexPath = path.join(
|
|
749
|
+
import.meta.dirname,
|
|
750
|
+
"..",
|
|
751
|
+
"studio",
|
|
752
|
+
"dist",
|
|
753
|
+
"index.html"
|
|
754
|
+
);
|
|
755
|
+
try {
|
|
756
|
+
const studioFile = Bun.file(studioIndexPath);
|
|
757
|
+
if (await studioFile.exists()) {
|
|
758
|
+
let html = await studioFile.text();
|
|
759
|
+
// Inject metadata into the HTML
|
|
760
|
+
const metadata = getSerializedMetadataStorage();
|
|
761
|
+
const metadataScript = `<script>window.bunsaneMetadata = ${JSON.stringify(
|
|
762
|
+
metadata
|
|
763
|
+
)};</script>`;
|
|
764
|
+
// Insert before the closing </head> tag
|
|
765
|
+
html = html.replace(
|
|
766
|
+
"</head>",
|
|
767
|
+
`${metadataScript}</head>`
|
|
768
|
+
);
|
|
769
|
+
return this.addCorsHeaders(new Response(html, {
|
|
770
|
+
headers: { "Content-Type": "text/html" },
|
|
771
|
+
}), req);
|
|
772
|
+
} else {
|
|
773
|
+
return this.addCorsHeaders(new Response(
|
|
774
|
+
"Studio not built. Run `bun run build:studio` to build the studio.",
|
|
775
|
+
{
|
|
776
|
+
status: 404,
|
|
777
|
+
headers: { "Content-Type": "text/plain" },
|
|
778
|
+
}
|
|
779
|
+
), req);
|
|
780
|
+
}
|
|
781
|
+
} catch (error) {
|
|
782
|
+
console.log("Error loading studio index.html:", error);
|
|
783
|
+
return this.addCorsHeaders(new Response("Studio not available", {
|
|
784
|
+
status: 404,
|
|
785
|
+
headers: { "Content-Type": "text/plain" },
|
|
786
|
+
}), req);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
297
789
|
}
|
|
298
790
|
for (const [route, folder] of this.staticAssets) {
|
|
299
791
|
if (url.pathname.startsWith(route)) {
|
|
@@ -303,39 +795,71 @@ export default class App {
|
|
|
303
795
|
const file = Bun.file(filePath);
|
|
304
796
|
if (await file.exists()) {
|
|
305
797
|
clearTimeout(timeoutId);
|
|
306
|
-
return new Response(file);
|
|
798
|
+
return this.addCorsHeaders(new Response(file), req);
|
|
307
799
|
}
|
|
308
800
|
} catch (error) {
|
|
309
|
-
logger.error(
|
|
801
|
+
logger.error(
|
|
802
|
+
`Error serving static file ${filePath}:`,
|
|
803
|
+
error as any
|
|
804
|
+
);
|
|
310
805
|
}
|
|
311
806
|
}
|
|
312
807
|
}
|
|
313
808
|
|
|
314
809
|
// Lookup REST endpoint using map for O(1) performance
|
|
315
810
|
const endpointKey = `${method}:${url.pathname}`;
|
|
316
|
-
|
|
811
|
+
let endpoint = this.restEndpointMap.get(endpointKey);
|
|
812
|
+
|
|
813
|
+
// If exact match not found, try pattern matching for parameterized routes
|
|
814
|
+
if (!endpoint) {
|
|
815
|
+
for (const ep of this.restEndpoints) {
|
|
816
|
+
if (ep.method !== method) continue;
|
|
817
|
+
// Convert route pattern to regex (e.g., /api/v1/users/:id -> /api/v1/users/[^/]+)
|
|
818
|
+
const pattern = ep.path.replace(/:[^/]+/g, '[^/]+');
|
|
819
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
820
|
+
if (regex.test(url.pathname)) {
|
|
821
|
+
endpoint = ep;
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
317
827
|
if (endpoint) {
|
|
318
828
|
try {
|
|
319
829
|
const result = await endpoint.handler(req);
|
|
320
830
|
const duration = Date.now() - startTime;
|
|
321
|
-
logger.trace(
|
|
322
|
-
|
|
831
|
+
logger.trace(
|
|
832
|
+
`REST ${method} ${url.pathname} completed in ${duration}ms`
|
|
833
|
+
);
|
|
834
|
+
|
|
323
835
|
clearTimeout(timeoutId);
|
|
324
836
|
if (result instanceof Response) {
|
|
325
|
-
return result;
|
|
837
|
+
return this.addCorsHeaders(result, req);
|
|
326
838
|
} else {
|
|
327
|
-
return new Response(JSON.stringify(result), {
|
|
328
|
-
headers: {
|
|
329
|
-
});
|
|
839
|
+
return this.addCorsHeaders(new Response(JSON.stringify(result), {
|
|
840
|
+
headers: { "Content-Type": "application/json" },
|
|
841
|
+
}), req);
|
|
330
842
|
}
|
|
331
843
|
} catch (error) {
|
|
332
844
|
const duration = Date.now() - startTime;
|
|
333
|
-
logger.error(
|
|
845
|
+
logger.error(
|
|
846
|
+
`Error in REST endpoint ${method} ${endpoint.path} after ${duration}ms`,
|
|
847
|
+
error as any
|
|
848
|
+
);
|
|
334
849
|
clearTimeout(timeoutId);
|
|
335
|
-
return new Response(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
850
|
+
return this.addCorsHeaders(new Response(
|
|
851
|
+
JSON.stringify({
|
|
852
|
+
error: "Internal server error",
|
|
853
|
+
code: "INTERNAL_ERROR",
|
|
854
|
+
...(process.env.NODE_ENV === 'development' && {
|
|
855
|
+
message: (error as Error)?.message,
|
|
856
|
+
}),
|
|
857
|
+
}),
|
|
858
|
+
{
|
|
859
|
+
status: 500,
|
|
860
|
+
headers: { "Content-Type": "application/json" },
|
|
861
|
+
}
|
|
862
|
+
), req);
|
|
339
863
|
}
|
|
340
864
|
}
|
|
341
865
|
|
|
@@ -348,23 +872,38 @@ export default class App {
|
|
|
348
872
|
}
|
|
349
873
|
|
|
350
874
|
clearTimeout(timeoutId);
|
|
351
|
-
return new Response(
|
|
875
|
+
return this.addCorsHeaders(new Response("Not Found", { status: 404 }), req);
|
|
352
876
|
} catch (error) {
|
|
353
877
|
const duration = Date.now() - startTime;
|
|
354
|
-
logger.error(
|
|
878
|
+
logger.error(
|
|
879
|
+
`Request failed after ${duration}ms: ${method} ${url.pathname}`,
|
|
880
|
+
error as any
|
|
881
|
+
);
|
|
355
882
|
clearTimeout(timeoutId);
|
|
356
|
-
|
|
357
|
-
if ((error as Error).name ===
|
|
358
|
-
return new Response(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
883
|
+
|
|
884
|
+
if ((error as Error).name === "AbortError") {
|
|
885
|
+
return this.addCorsHeaders(new Response(
|
|
886
|
+
JSON.stringify({ error: "Request timeout", code: "TIMEOUT_ERROR" }),
|
|
887
|
+
{
|
|
888
|
+
status: 408,
|
|
889
|
+
headers: { "Content-Type": "application/json" },
|
|
890
|
+
}
|
|
891
|
+
), req);
|
|
362
892
|
}
|
|
363
|
-
|
|
364
|
-
return new Response(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
893
|
+
|
|
894
|
+
return this.addCorsHeaders(new Response(
|
|
895
|
+
JSON.stringify({
|
|
896
|
+
error: "Internal server error",
|
|
897
|
+
code: "INTERNAL_ERROR",
|
|
898
|
+
...(process.env.NODE_ENV === 'development' && {
|
|
899
|
+
message: (error as Error)?.message,
|
|
900
|
+
}),
|
|
901
|
+
}),
|
|
902
|
+
{
|
|
903
|
+
status: 500,
|
|
904
|
+
headers: { "Content-Type": "application/json" },
|
|
905
|
+
}
|
|
906
|
+
), req);
|
|
368
907
|
}
|
|
369
908
|
}
|
|
370
909
|
|
|
@@ -384,19 +923,227 @@ export default class App {
|
|
|
384
923
|
this.enforceDocs = value;
|
|
385
924
|
}
|
|
386
925
|
|
|
926
|
+
public enableStudio() {
|
|
927
|
+
this.studioEnabled = true;
|
|
928
|
+
logger.info("Studio API enabled");
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Set the maximum allowed GraphQL query depth. 0 disables the limit.
|
|
933
|
+
*/
|
|
934
|
+
public setGraphQLMaxDepth(depth: number) {
|
|
935
|
+
this.graphqlMaxDepth = depth;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Set the grace period for draining connections during shutdown (ms).
|
|
940
|
+
*/
|
|
941
|
+
public setShutdownGracePeriod(ms: number) {
|
|
942
|
+
this.shutdownGracePeriod = ms;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Set the maximum request body size in bytes (default: 50MB).
|
|
947
|
+
* Rejects oversized requests at the HTTP layer before buffering.
|
|
948
|
+
*/
|
|
949
|
+
public setMaxRequestBodySize(bytes: number) {
|
|
950
|
+
this.maxRequestBodySize = bytes;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Warm up the prepared statement cache with common query patterns
|
|
955
|
+
*/
|
|
956
|
+
private async warmUpPreparedStatementCache(): Promise<void> {
|
|
957
|
+
// Get registered components for generating common queries
|
|
958
|
+
const components = ComponentRegistry.getComponents();
|
|
959
|
+
|
|
960
|
+
if (components.length === 0) {
|
|
961
|
+
logger.trace(
|
|
962
|
+
"No components registered yet, skipping cache warm-up"
|
|
963
|
+
);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const commonQueries: Array<{ sql: string; key: string }> = [];
|
|
968
|
+
|
|
969
|
+
// Generate some common query patterns
|
|
970
|
+
// 1. Simple entity count
|
|
971
|
+
commonQueries.push({
|
|
972
|
+
sql: "SELECT COUNT(*) as count FROM (SELECT DISTINCT ec.entity_id as id FROM entity_components ec WHERE ec.deleted_at IS NULL) AS subquery",
|
|
973
|
+
key: "count_all_entities",
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// 2. Common component queries (first few components)
|
|
977
|
+
for (let i = 0; i < Math.min(5, components.length); i++) {
|
|
978
|
+
const component = components[i];
|
|
979
|
+
if (component) {
|
|
980
|
+
const { name, ctor } = component;
|
|
981
|
+
const typeId = ComponentRegistry.getComponentId(name);
|
|
982
|
+
if (typeId) {
|
|
983
|
+
commonQueries.push({
|
|
984
|
+
sql: `SELECT DISTINCT ec.entity_id as id FROM entity_components ec WHERE ec.type_id = '${typeId}' AND ec.deleted_at IS NULL LIMIT 10`,
|
|
985
|
+
key: `find_${name.toLowerCase()}_sample`,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// 3. Multi-component queries (if we have multiple components)
|
|
992
|
+
if (components.length >= 2) {
|
|
993
|
+
const typeIds = components
|
|
994
|
+
.slice(0, 3)
|
|
995
|
+
.map((component: { name: string; ctor: any }) =>
|
|
996
|
+
ComponentRegistry.getComponentId(component.name)
|
|
997
|
+
)
|
|
998
|
+
.filter((id: string | undefined) => id)
|
|
999
|
+
.join("','");
|
|
1000
|
+
|
|
1001
|
+
if (typeIds) {
|
|
1002
|
+
commonQueries.push({
|
|
1003
|
+
sql: `SELECT DISTINCT ec.entity_id as id FROM entity_components ec WHERE ec.type_id IN ('${typeIds}') AND ec.deleted_at IS NULL LIMIT 10`,
|
|
1004
|
+
key: "find_multi_component_sample",
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
await preparedStatementCache.warmUp(commonQueries, db);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private async collectMetrics() {
|
|
1013
|
+
let cacheStats = null;
|
|
1014
|
+
try {
|
|
1015
|
+
const { CacheManager } = await import('./cache/CacheManager');
|
|
1016
|
+
cacheStats = await CacheManager.getInstance().getStats();
|
|
1017
|
+
} catch {}
|
|
1018
|
+
|
|
1019
|
+
return {
|
|
1020
|
+
timestamp: new Date().toISOString(),
|
|
1021
|
+
uptime: process.uptime(),
|
|
1022
|
+
process: process.memoryUsage(),
|
|
1023
|
+
cache: cacheStats,
|
|
1024
|
+
scheduler: SchedulerManager.getInstance().getMetrics(),
|
|
1025
|
+
preparedStatements: preparedStatementCache.getStats(),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
387
1029
|
async start() {
|
|
388
1030
|
logger.info("Application Started");
|
|
389
|
-
const port = parseInt(process.env.
|
|
390
|
-
|
|
1031
|
+
const port = parseInt(process.env.APP_PORT || "3000");
|
|
1032
|
+
|
|
1033
|
+
// Read env overrides
|
|
1034
|
+
const envGracePeriod = process.env.SHUTDOWN_GRACE_PERIOD_MS;
|
|
1035
|
+
if (envGracePeriod) {
|
|
1036
|
+
this.shutdownGracePeriod = parseInt(envGracePeriod, 10);
|
|
1037
|
+
}
|
|
1038
|
+
const envBodySize = process.env.MAX_REQUEST_BODY_SIZE;
|
|
1039
|
+
if (envBodySize) {
|
|
1040
|
+
this.maxRequestBodySize = parseInt(envBodySize, 10);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Compose middleware chain around the core request handler
|
|
1044
|
+
this.composedHandler = composeMiddleware(
|
|
1045
|
+
this.middlewares,
|
|
1046
|
+
this.handleRequest.bind(this),
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
this.server = Bun.serve({
|
|
1050
|
+
idleTimeout: 0, // Disable idle timeout because we have subscriptions
|
|
391
1051
|
port: port,
|
|
392
|
-
|
|
1052
|
+
maxRequestBodySize: this.maxRequestBodySize,
|
|
1053
|
+
fetch: this.composedHandler,
|
|
393
1054
|
});
|
|
394
|
-
|
|
1055
|
+
|
|
395
1056
|
// Update the OpenAPI spec with the actual server URL
|
|
396
|
-
this.openAPISpecGenerator!.addServer(
|
|
397
|
-
|
|
398
|
-
|
|
1057
|
+
this.openAPISpecGenerator!.addServer(
|
|
1058
|
+
`http://localhost:${port}`,
|
|
1059
|
+
"Development server"
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
logger.info(
|
|
1063
|
+
`Server is running on ${new URL(
|
|
1064
|
+
this.yoga?.graphqlEndpoint || "/graphql",
|
|
1065
|
+
`http://${this.server.hostname}:${this.server.port}`
|
|
1066
|
+
)}`
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
// Register signal handlers for graceful shutdown
|
|
1070
|
+
process.on('SIGTERM', async () => {
|
|
1071
|
+
logger.info({ scope: 'app', component: 'App', msg: 'Received SIGTERM' });
|
|
1072
|
+
await this.shutdown();
|
|
1073
|
+
process.exit(0);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
process.on('SIGINT', async () => {
|
|
1077
|
+
logger.info({ scope: 'app', component: 'App', msg: 'Received SIGINT' });
|
|
1078
|
+
await this.shutdown();
|
|
1079
|
+
process.exit(0);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
// Global error handlers to prevent silent crashes
|
|
1083
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
1084
|
+
logger.error({ scope: 'app', component: 'App', reason, msg: 'Unhandled promise rejection' });
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
process.on('uncaughtException', (error) => {
|
|
1088
|
+
logger.fatal({ scope: 'app', component: 'App', error, msg: 'Uncaught exception — shutting down' });
|
|
1089
|
+
this.shutdown().finally(() => process.exit(1));
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
this.isReady = true;
|
|
1093
|
+
this.appReadyCallbacks.forEach((cb) => cb());
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Gracefully shutdown the application
|
|
1098
|
+
*/
|
|
1099
|
+
async shutdown(): Promise<void> {
|
|
1100
|
+
if (this.isShuttingDown) return;
|
|
1101
|
+
this.isShuttingDown = true;
|
|
1102
|
+
this.isReady = false;
|
|
1103
|
+
|
|
1104
|
+
logger.info({ scope: 'app', component: 'App', msg: 'Shutting down application' });
|
|
1105
|
+
|
|
1106
|
+
// Stop HTTP server — drain then force-close after grace period
|
|
1107
|
+
if (this.server) {
|
|
1108
|
+
try {
|
|
1109
|
+
logger.info({ scope: 'app', component: 'App', msg: 'Draining connections' });
|
|
1110
|
+
this.server.stop(false);
|
|
1111
|
+
const forceTimer = setTimeout(() => {
|
|
1112
|
+
logger.warn({ scope: 'app', component: 'App', msg: 'Grace period expired, forcing connection close' });
|
|
1113
|
+
try { this.server?.stop(true); } catch {}
|
|
1114
|
+
}, this.shutdownGracePeriod);
|
|
1115
|
+
forceTimer.unref?.();
|
|
1116
|
+
logger.info({ scope: 'app', component: 'App', msg: 'HTTP server stopped' });
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
logger.warn({ scope: 'app', component: 'App', msg: 'HTTP server stop error', error });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Stop scheduler
|
|
1123
|
+
try {
|
|
1124
|
+
await SchedulerManager.getInstance().stop();
|
|
1125
|
+
logger.info({ scope: 'app', component: 'App', msg: 'Scheduler stopped' });
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
logger.warn({ scope: 'app', component: 'App', msg: 'Scheduler stop error', error });
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Shutdown cache
|
|
1131
|
+
try {
|
|
1132
|
+
const { CacheManager } = await import('./cache/CacheManager');
|
|
1133
|
+
await CacheManager.getInstance().shutdown();
|
|
1134
|
+
logger.info({ scope: 'cache', component: 'App', msg: 'Cache shutdown completed' });
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
logger.warn({ scope: 'cache', component: 'App', msg: 'Cache shutdown error', error });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Close database pool (last step)
|
|
1140
|
+
try {
|
|
1141
|
+
db.close();
|
|
1142
|
+
logger.info({ scope: 'app', component: 'App', msg: 'Database pool closed' });
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
logger.warn({ scope: 'app', component: 'App', msg: 'Database pool close error', error });
|
|
1145
|
+
}
|
|
399
1146
|
|
|
400
|
-
|
|
1147
|
+
logger.info({ scope: 'app', component: 'App', msg: 'Application shutdown completed' });
|
|
401
1148
|
}
|
|
402
|
-
}
|
|
1149
|
+
}
|