bunsane 0.1.0 → 0.1.2
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 +168 -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 +193 -14
- 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 +503 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +89 -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 +5 -5
- 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/gql/Generator.ts +58 -35
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +65 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +19 -10
- 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 +338 -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/core/Events.ts +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Deploy Documentation to GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
paths:
|
|
7
|
+
- 'docs/**'
|
|
8
|
+
- '.github/workflows/deploy-docs.yml'
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: [ main ]
|
|
11
|
+
paths:
|
|
12
|
+
- 'docs/**'
|
|
13
|
+
- '.github/workflows/deploy-docs.yml'
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build-and-deploy:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Setup Node.js
|
|
24
|
+
uses: actions/setup-node@v4
|
|
25
|
+
with:
|
|
26
|
+
node-version: '18'
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: |
|
|
30
|
+
cd docs
|
|
31
|
+
npm install -g docsify-cli@latest
|
|
32
|
+
|
|
33
|
+
- name: Build documentation
|
|
34
|
+
run: |
|
|
35
|
+
cd docs
|
|
36
|
+
# Generate any dynamic content if needed
|
|
37
|
+
echo "Building documentation..."
|
|
38
|
+
|
|
39
|
+
- name: Deploy to GitHub Pages
|
|
40
|
+
if: github.ref == 'refs/heads/main'
|
|
41
|
+
uses: peaceiris/actions-gh-pages@v3
|
|
42
|
+
with:
|
|
43
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
44
|
+
publish_dir: ./docs
|
|
45
|
+
|
|
46
|
+
validate-links:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- name: Checkout
|
|
50
|
+
uses: actions/checkout@v4
|
|
51
|
+
|
|
52
|
+
- name: Link Checker
|
|
53
|
+
uses: lycheeverse/lychee-action@v1.8.0
|
|
54
|
+
with:
|
|
55
|
+
args: --config docs/.lychee.toml docs/
|
|
56
|
+
env:
|
|
57
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
package/LICENSE.md
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="https://raw.githubusercontent.com/yaaruu/bunsane/refs/heads/main/BunSane.jpg" alt="BunSane" width="520" />
|
|
4
4
|
|
|
5
5
|
# BunSane — Batteries‑included TypeScript API framework for Bun
|
|
6
6
|
|
|
@@ -20,35 +20,9 @@
|
|
|
20
20
|
- Pino logging, pretty mode in development
|
|
21
21
|
- Zod-friendly GraphQL error helper
|
|
22
22
|
|
|
23
|
-
## Install
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
Full documentation visit: [Documentation](https://yaaruu.github.io/bunsane/#/)
|
|
26
25
|
|
|
27
|
-
```cmd
|
|
28
|
-
bun install @yaaruu/bunsane
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Ensure your tsconfig enables decorators in your app:
|
|
32
|
-
|
|
33
|
-
```json
|
|
34
|
-
{
|
|
35
|
-
"compilerOptions": {
|
|
36
|
-
"experimentalDecorators": true,
|
|
37
|
-
"emitDecoratorMetadata": true
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Full documentation visit: [Documentation](https://example.com)
|
|
43
|
-
|
|
44
|
-
## Core concepts
|
|
45
|
-
|
|
46
|
-
### ECS ( Entity Component Services )
|
|
47
|
-
TODO
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## LICENSE
|
|
51
|
-
MIT
|
|
52
26
|
|
|
53
27
|
---
|
|
54
28
|
|
package/TODO.md
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
//
|
|
1
|
+
// CORE FEATURE
|
|
2
|
+
TODO: Custom Component Table with Direct Column instead of jsonb
|
|
3
|
+
TODO: Add GraphQL type registration for Interface, Enum, etc
|
|
4
|
+
|
|
5
|
+
// QOL FEATURE
|
|
6
|
+
TODO: Filesystem
|
|
7
|
+
TODO: Field Constraint
|
|
8
|
+
TODO: Add OpenAPI Swagger Spec
|
package/bun.lock
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"": {
|
|
5
5
|
"name": "bunsane",
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"dataloader": "^2.2.2",
|
|
7
8
|
"graphql": "^16.11.0",
|
|
8
9
|
"graphql-yoga": "^5.15.1",
|
|
9
10
|
"pino": "^9.9.0",
|
|
@@ -74,6 +75,8 @@
|
|
|
74
75
|
|
|
75
76
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
|
76
77
|
|
|
78
|
+
"dataloader": ["dataloader@2.2.3", "", {}, "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA=="],
|
|
79
|
+
|
|
77
80
|
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
|
|
78
81
|
|
|
79
82
|
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { UploadConfiguration } from "../types/upload.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default Upload Configuration
|
|
5
|
+
* Contains sensible defaults for the upload system
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_UPLOAD_CONFIG: UploadConfiguration = {
|
|
8
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
9
|
+
allowedMimeTypes: [
|
|
10
|
+
// Images
|
|
11
|
+
"image/jpeg",
|
|
12
|
+
"image/png",
|
|
13
|
+
"image/gif",
|
|
14
|
+
"image/webp",
|
|
15
|
+
"image/svg+xml",
|
|
16
|
+
// Documents
|
|
17
|
+
"application/pdf",
|
|
18
|
+
"text/plain",
|
|
19
|
+
"application/msword",
|
|
20
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
21
|
+
],
|
|
22
|
+
allowedExtensions: [
|
|
23
|
+
// Images
|
|
24
|
+
".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg",
|
|
25
|
+
// Documents
|
|
26
|
+
".pdf", ".txt", ".doc", ".docx"
|
|
27
|
+
],
|
|
28
|
+
validateFileSignature: true,
|
|
29
|
+
sanitizeFileName: true,
|
|
30
|
+
preserveOriginalName: false,
|
|
31
|
+
generateThumbnails: false,
|
|
32
|
+
uploadPath: "uploads",
|
|
33
|
+
namingStrategy: "uuid",
|
|
34
|
+
imageProcessing: {
|
|
35
|
+
generateThumbnails: false,
|
|
36
|
+
thumbnailSizes: [
|
|
37
|
+
{ width: 150, height: 150, suffix: "_thumb" },
|
|
38
|
+
{ width: 300, height: 300, suffix: "_medium" },
|
|
39
|
+
{ width: 800, height: 600, suffix: "_large" }
|
|
40
|
+
],
|
|
41
|
+
compress: true,
|
|
42
|
+
quality: 85,
|
|
43
|
+
maxDimensions: { width: 2048, height: 2048 }
|
|
44
|
+
},
|
|
45
|
+
validation: {
|
|
46
|
+
scanForMalware: false,
|
|
47
|
+
strictMimeType: true,
|
|
48
|
+
customValidators: []
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Image-specific upload configuration
|
|
54
|
+
*/
|
|
55
|
+
export const IMAGE_UPLOAD_CONFIG: Partial<UploadConfiguration> = {
|
|
56
|
+
maxFileSize: 5 * 1024 * 1024, // 5MB
|
|
57
|
+
allowedMimeTypes: [
|
|
58
|
+
"image/jpeg",
|
|
59
|
+
"image/png",
|
|
60
|
+
"image/gif",
|
|
61
|
+
"image/webp"
|
|
62
|
+
],
|
|
63
|
+
allowedExtensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
|
|
64
|
+
generateThumbnails: true,
|
|
65
|
+
imageProcessing: {
|
|
66
|
+
generateThumbnails: true,
|
|
67
|
+
thumbnailSizes: [
|
|
68
|
+
{ width: 150, height: 150, suffix: "_thumb" },
|
|
69
|
+
{ width: 300, height: 300, suffix: "_medium" }
|
|
70
|
+
],
|
|
71
|
+
compress: true,
|
|
72
|
+
quality: 85,
|
|
73
|
+
maxDimensions: { width: 1920, height: 1080 }
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Document upload configuration
|
|
79
|
+
*/
|
|
80
|
+
export const DOCUMENT_UPLOAD_CONFIG: Partial<UploadConfiguration> = {
|
|
81
|
+
maxFileSize: 25 * 1024 * 1024, // 25MB
|
|
82
|
+
allowedMimeTypes: [
|
|
83
|
+
"application/pdf",
|
|
84
|
+
"text/plain",
|
|
85
|
+
"application/msword",
|
|
86
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
87
|
+
"application/vnd.ms-excel",
|
|
88
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
89
|
+
],
|
|
90
|
+
allowedExtensions: [".pdf", ".txt", ".doc", ".docx", ".xls", ".xlsx"],
|
|
91
|
+
validateFileSignature: true,
|
|
92
|
+
generateThumbnails: false,
|
|
93
|
+
validation: {
|
|
94
|
+
scanForMalware: true,
|
|
95
|
+
strictMimeType: true
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Avatar/profile picture configuration
|
|
101
|
+
*/
|
|
102
|
+
export const AVATAR_UPLOAD_CONFIG: Partial<UploadConfiguration> = {
|
|
103
|
+
maxFileSize: 2 * 1024 * 1024, // 2MB
|
|
104
|
+
allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
|
|
105
|
+
allowedExtensions: [".jpg", ".jpeg", ".png", ".webp"],
|
|
106
|
+
generateThumbnails: true,
|
|
107
|
+
imageProcessing: {
|
|
108
|
+
generateThumbnails: true,
|
|
109
|
+
thumbnailSizes: [
|
|
110
|
+
{ width: 50, height: 50, suffix: "_small" },
|
|
111
|
+
{ width: 150, height: 150, suffix: "_medium" },
|
|
112
|
+
{ width: 300, height: 300, suffix: "_large" }
|
|
113
|
+
],
|
|
114
|
+
compress: true,
|
|
115
|
+
quality: 90,
|
|
116
|
+
maxDimensions: { width: 800, height: 800 }
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Strict security configuration for public uploads
|
|
122
|
+
*/
|
|
123
|
+
export const SECURE_UPLOAD_CONFIG: Partial<UploadConfiguration> = {
|
|
124
|
+
maxFileSize: 1 * 1024 * 1024, // 1MB
|
|
125
|
+
allowedMimeTypes: ["image/jpeg", "image/png"],
|
|
126
|
+
allowedExtensions: [".jpg", ".jpeg", ".png"],
|
|
127
|
+
validateFileSignature: true,
|
|
128
|
+
sanitizeFileName: true,
|
|
129
|
+
preserveOriginalName: false,
|
|
130
|
+
validation: {
|
|
131
|
+
scanForMalware: true,
|
|
132
|
+
strictMimeType: true,
|
|
133
|
+
customValidators: []
|
|
134
|
+
}
|
|
135
|
+
};
|
package/core/App.ts
CHANGED
|
@@ -4,9 +4,18 @@ import ComponentRegistry from "core/ComponentRegistry";
|
|
|
4
4
|
import { logger } from "core/Logger";
|
|
5
5
|
import { createYogaInstance } from "gql";
|
|
6
6
|
import ServiceRegistry from "service/ServiceRegistry";
|
|
7
|
+
import type { Plugin } from "graphql-yoga";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { registerDecoratedHooks } from "core/decorators/EntityHooks";
|
|
10
|
+
import { SchedulerManager } from "core/SchedulerManager";
|
|
11
|
+
import { registerScheduledTasks } from "core/decorators/ScheduledTask";
|
|
7
12
|
|
|
8
13
|
export default class App {
|
|
9
14
|
private yoga: any;
|
|
15
|
+
private yogaPlugins: Plugin[] = [];
|
|
16
|
+
private restEndpoints: Array<{ method: string; path: string; handler: Function; service: any }> = [];
|
|
17
|
+
private restEndpointMap: Map<string, { method: string; path: string; handler: Function; service: any }> = new Map();
|
|
18
|
+
private staticAssets: Map<string, string> = new Map();
|
|
10
19
|
|
|
11
20
|
constructor() {
|
|
12
21
|
this.init();
|
|
@@ -32,6 +41,18 @@ export default class App {
|
|
|
32
41
|
break;
|
|
33
42
|
}
|
|
34
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
|
+
|
|
35
56
|
ApplicationLifecycle.setPhase(ApplicationPhase.SYSTEM_REGISTERING);
|
|
36
57
|
break;
|
|
37
58
|
}
|
|
@@ -39,10 +60,45 @@ export default class App {
|
|
|
39
60
|
try {
|
|
40
61
|
const schema = ServiceRegistry.getSchema();
|
|
41
62
|
if (schema) {
|
|
42
|
-
this.yoga = createYogaInstance(schema);
|
|
63
|
+
this.yoga = createYogaInstance(schema, this.yogaPlugins);
|
|
43
64
|
} else {
|
|
44
|
-
this.yoga = createYogaInstance();
|
|
65
|
+
this.yoga = createYogaInstance(undefined, this.yogaPlugins);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Get all services for processing
|
|
69
|
+
const services = ServiceRegistry.getServices();
|
|
70
|
+
|
|
71
|
+
// Initialize Scheduler
|
|
72
|
+
const scheduler = SchedulerManager.getInstance();
|
|
73
|
+
|
|
74
|
+
// Register scheduled tasks for all services
|
|
75
|
+
for (const service of services) {
|
|
76
|
+
try {
|
|
77
|
+
registerScheduledTasks(service);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.warn(`Failed to register scheduled tasks for service ${service.constructor.name}`);
|
|
80
|
+
logger.warn(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
logger.info(`Registered scheduled tasks for ${services.length} services`);
|
|
84
|
+
|
|
85
|
+
// Collect REST endpoints from all services
|
|
86
|
+
for (const service of services) {
|
|
87
|
+
const endpoints = (service.constructor as any).httpEndpoints;
|
|
88
|
+
if (endpoints) {
|
|
89
|
+
for (const endpoint of endpoints) {
|
|
90
|
+
const endpointInfo = {
|
|
91
|
+
method: endpoint.method,
|
|
92
|
+
path: endpoint.path,
|
|
93
|
+
handler: endpoint.handler.bind(service),
|
|
94
|
+
service: service
|
|
95
|
+
};
|
|
96
|
+
this.restEndpoints.push(endpointInfo);
|
|
97
|
+
this.restEndpointMap.set(`${endpoint.method}:${endpoint.path}`, endpointInfo);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
45
100
|
}
|
|
101
|
+
|
|
46
102
|
ApplicationLifecycle.setPhase(ApplicationPhase.APPLICATION_READY);
|
|
47
103
|
} catch (error) {
|
|
48
104
|
logger.error("Error during SYSTEM_READY phase:");
|
|
@@ -71,11 +127,119 @@ export default class App {
|
|
|
71
127
|
});
|
|
72
128
|
}
|
|
73
129
|
|
|
130
|
+
public addYogaPlugin(plugin: Plugin) {
|
|
131
|
+
this.yogaPlugins.push(plugin);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public addStaticAssets(route: string, folder: string) {
|
|
135
|
+
// Resolve the folder path relative to the current working directory
|
|
136
|
+
const resolvedFolder = path.resolve(folder);
|
|
137
|
+
this.staticAssets.set(route, resolvedFolder);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async handleRequest(req: Request): Promise<Response> {
|
|
141
|
+
const url = new URL(req.url);
|
|
142
|
+
const method = req.method;
|
|
143
|
+
const startTime = Date.now();
|
|
144
|
+
|
|
145
|
+
// Add request timeout
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const timeoutId = setTimeout(() => {
|
|
148
|
+
controller.abort();
|
|
149
|
+
logger.warn(`Request timeout: ${method} ${url.pathname}`);
|
|
150
|
+
}, 30000); // 30 second timeout
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// Health check endpoint
|
|
154
|
+
if (url.pathname === '/health') {
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
return new Response(JSON.stringify({
|
|
157
|
+
status: 'ok',
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
uptime: process.uptime()
|
|
160
|
+
}), {
|
|
161
|
+
headers: { 'Content-Type': 'application/json' }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const [route, folder] of this.staticAssets) {
|
|
165
|
+
if (url.pathname.startsWith(route)) {
|
|
166
|
+
const relativePath = url.pathname.slice(route.length);
|
|
167
|
+
const filePath = path.join(folder, relativePath);
|
|
168
|
+
try {
|
|
169
|
+
const file = Bun.file(filePath);
|
|
170
|
+
if (await file.exists()) {
|
|
171
|
+
clearTimeout(timeoutId);
|
|
172
|
+
return new Response(file);
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
logger.error(`Error serving static file ${filePath}:`, error as any);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Lookup REST endpoint using map for O(1) performance
|
|
181
|
+
const endpointKey = `${method}:${url.pathname}`;
|
|
182
|
+
const endpoint = this.restEndpointMap.get(endpointKey);
|
|
183
|
+
if (endpoint) {
|
|
184
|
+
try {
|
|
185
|
+
const result = await endpoint.handler(req);
|
|
186
|
+
const duration = Date.now() - startTime;
|
|
187
|
+
logger.trace(`REST ${method} ${url.pathname} completed in ${duration}ms`);
|
|
188
|
+
|
|
189
|
+
clearTimeout(timeoutId);
|
|
190
|
+
if (result instanceof Response) {
|
|
191
|
+
return result;
|
|
192
|
+
} else {
|
|
193
|
+
return new Response(JSON.stringify(result), {
|
|
194
|
+
headers: { 'Content-Type': 'application/json' }
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const duration = Date.now() - startTime;
|
|
199
|
+
logger.error(`Error in REST endpoint ${method} ${endpoint.path} after ${duration}ms`, error as any);
|
|
200
|
+
clearTimeout(timeoutId);
|
|
201
|
+
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
202
|
+
status: 500,
|
|
203
|
+
headers: { 'Content-Type': 'application/json' }
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this.yoga) {
|
|
209
|
+
const response = await this.yoga(req);
|
|
210
|
+
const duration = Date.now() - startTime;
|
|
211
|
+
logger.trace(`GraphQL request completed in ${duration}ms`);
|
|
212
|
+
clearTimeout(timeoutId);
|
|
213
|
+
return response;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
clearTimeout(timeoutId);
|
|
217
|
+
return new Response('Not Found', { status: 404 });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const duration = Date.now() - startTime;
|
|
220
|
+
logger.error(`Request failed after ${duration}ms: ${method} ${url.pathname}`, error as any);
|
|
221
|
+
clearTimeout(timeoutId);
|
|
222
|
+
|
|
223
|
+
if ((error as Error).name === 'AbortError') {
|
|
224
|
+
return new Response(JSON.stringify({ error: 'Request timeout' }), {
|
|
225
|
+
status: 408,
|
|
226
|
+
headers: { 'Content-Type': 'application/json' }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
231
|
+
status: 500,
|
|
232
|
+
headers: { 'Content-Type': 'application/json' }
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
74
237
|
async start() {
|
|
75
238
|
logger.info("Application Started");
|
|
76
239
|
const server = Bun.serve({
|
|
77
|
-
|
|
240
|
+
port: parseInt(process.env.PORT || "3000"),
|
|
241
|
+
fetch: this.handleRequest.bind(this),
|
|
78
242
|
});
|
|
79
|
-
logger.info(`Server is running on ${new URL(this.yoga
|
|
243
|
+
logger.info(`Server is running on ${new URL(this.yoga?.graphqlEndpoint || '/graphql', `http://${server.hostname}:${server.port}`)}`)
|
|
80
244
|
}
|
|
81
245
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { BaseComponent, ComponentDataType } from "./Components";
|
|
2
|
+
import { Entity } from "./Entity";
|
|
3
|
+
|
|
4
|
+
function compNameToFieldName(compName: string): string {
|
|
5
|
+
return compName.charAt(0).toLowerCase() + compName.slice(1).replace(/Component$/, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ArcheType provides a layer of abstraction for creating entities with predefined sets of components.
|
|
10
|
+
* This makes entity creation more elegant and reduces code repetition.
|
|
11
|
+
*
|
|
12
|
+
* Example usage:
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const UserArcheType = new ArcheType([NameComponent, EmailComponent, PasswordComponent]);
|
|
15
|
+
*
|
|
16
|
+
*
|
|
17
|
+
* // FROM Request or other source
|
|
18
|
+
* const userInput = { name: "John Doe", email: "john@example.com", password: "securepassword" };
|
|
19
|
+
* const entity = UserArcheType.fill(userInput).createEntity();
|
|
20
|
+
* await entity.save();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ArcheType {
|
|
26
|
+
protected components: Set<{ ctor: new (...args: any[]) => BaseComponent, data: any }> = new Set();
|
|
27
|
+
protected componentMap: Record<string, typeof BaseComponent> = {};
|
|
28
|
+
|
|
29
|
+
constructor(components: Array<new (...args: any[]) => BaseComponent>) {
|
|
30
|
+
for (const ctor of components) {
|
|
31
|
+
this.componentMap[compNameToFieldName(ctor.name)] = ctor;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
private addComponent<T extends BaseComponent>(ctor: new (...args: any[]) => T, data: ComponentDataType<T>) {
|
|
37
|
+
this.componentMap[compNameToFieldName(ctor.name)] = ctor;
|
|
38
|
+
this.components.add({ ctor, data });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
// TODO: Can we make this type-safe?
|
|
43
|
+
public fill(input: object, strict: boolean = false): this {
|
|
44
|
+
for (const [key, value] of Object.entries(input)) {
|
|
45
|
+
if (value !== undefined) {
|
|
46
|
+
const compCtor = this.componentMap[key];
|
|
47
|
+
if (compCtor) {
|
|
48
|
+
this.addComponent(compCtor, { value });
|
|
49
|
+
} else {
|
|
50
|
+
if (strict) {
|
|
51
|
+
throw new Error(`Component for field '${key}' not found in archetype.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const [field, ctor] of Object.entries(this.componentMap)) {
|
|
57
|
+
const alreadyAdded = Array.from(this.components).some(c => c.ctor === ctor);
|
|
58
|
+
if (!alreadyAdded) {
|
|
59
|
+
this.addComponent(ctor, {} as any);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async updateEntity<T>(entity: Entity, updates: Partial<T>) {
|
|
67
|
+
for (const key of Object.keys(updates)) {
|
|
68
|
+
if(key === 'id' || key === '_id') continue;
|
|
69
|
+
const value = updates[key as keyof T];
|
|
70
|
+
if (value !== undefined) {
|
|
71
|
+
const compCtor = this.componentMap[key];
|
|
72
|
+
if (compCtor) {
|
|
73
|
+
await entity.set(compCtor, { value });
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(`Component for field '${key}' not found in archetype.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a new entity with all the predefined components from this archetype.
|
|
83
|
+
* @returns A new Entity instance with all archetype components added
|
|
84
|
+
*/
|
|
85
|
+
public createEntity(): Entity {
|
|
86
|
+
const entity = Entity.Create();
|
|
87
|
+
for (const { ctor, data } of this.components) {
|
|
88
|
+
entity.add(ctor, data);
|
|
89
|
+
}
|
|
90
|
+
return entity;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a new entity and immediately saves it to the database.
|
|
95
|
+
* @returns A promise that resolves to the saved Entity
|
|
96
|
+
*/
|
|
97
|
+
public async createAndSaveEntity(): Promise<Entity> {
|
|
98
|
+
const entity = this.createEntity();
|
|
99
|
+
await entity.save();
|
|
100
|
+
return entity;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Unwraps an entity into a plain object containing the component data.
|
|
105
|
+
* @param entity The entity to unwrap
|
|
106
|
+
* @param exclude An optional array of field names to exclude from the result (e.g., sensitive data like passwords)
|
|
107
|
+
* @returns A promise that resolves to an object with component data
|
|
108
|
+
*/
|
|
109
|
+
public async Unwrap(entity: Entity, exclude: string[] = []): Promise<Record<string, any>> {
|
|
110
|
+
const result: any = { id: entity.id };
|
|
111
|
+
for (const [field, ctor] of Object.entries(this.componentMap)) {
|
|
112
|
+
if (exclude.includes(field)) continue;
|
|
113
|
+
const comp = await entity.get(ctor as any);
|
|
114
|
+
if (comp) {
|
|
115
|
+
result[field] = (comp as any).value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default ArcheType;
|