bunsane 0.1.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/BunSane.jpg +0 -0
- package/LICENSE.md +22 -0
- package/README.md +60 -0
- package/TODO.md +1 -0
- package/bun.lock +147 -0
- package/core/App.ts +81 -0
- package/core/ApplicationLifecycle.ts +57 -0
- package/core/ComponentRegistry.ts +116 -0
- package/core/Components.ts +124 -0
- package/core/Decorators.ts +20 -0
- package/core/Entity.ts +188 -0
- package/core/EntityManager.ts +46 -0
- package/core/ErrorHandler.ts +35 -0
- package/core/Events.ts +0 -0
- package/core/Logger.ts +16 -0
- package/core/Query.ts +296 -0
- package/database/DatabaseHelper.ts +138 -0
- package/database/index.ts +26 -0
- package/gql/Generator.ts +159 -0
- package/gql/index.ts +96 -0
- package/gql/types.ts +28 -0
- package/index.ts +22 -0
- package/package.json +28 -0
- package/service/Service.ts +7 -0
- package/service/ServiceRegistry.ts +51 -0
- package/service/index.ts +6 -0
- package/tests/query.test.ts +39 -0
- package/tsconfig.json +34 -0
- package/utils/uuid.ts +10 -0
package/BunSane.jpg
ADDED
|
Binary file
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BunSane contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="./BunSane.jpg" alt="BunSane" width="520" />
|
|
4
|
+
|
|
5
|
+
# BunSane — Batteries‑included TypeScript API framework for Bun
|
|
6
|
+
|
|
7
|
+
### Entity–Component storage on Postgres, a fluent query builder, and zero‑boilerplate GraphQL with GraphQL Yoga.
|
|
8
|
+
#### Skip Boilerplating and FOCUS writing Business Flow code 😉
|
|
9
|
+
|
|
10
|
+
### BunSane currently in `EXPERIMENTAL` state Not Production Ready
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Entity–Component model backed by PostgreSQL (auto-migrates base tables on first run)
|
|
16
|
+
- Declarative Components with decorators and indexed fields
|
|
17
|
+
- Fluent, performant Query builder (with/without population, filters, exclusions)
|
|
18
|
+
- Pluggable Services with decorators that generate a GraphQL schema automatically
|
|
19
|
+
- GraphQL Yoga server bootstrap out of the box
|
|
20
|
+
- Pino logging, pretty mode in development
|
|
21
|
+
- Zod-friendly GraphQL error helper
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Requires Bun and PostgreSQL.
|
|
26
|
+
|
|
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
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Made with⚡
|
|
56
|
+
- Bun
|
|
57
|
+
- GraphQL
|
|
58
|
+
- GraphQL Yoga
|
|
59
|
+
- PostgreSQL
|
|
60
|
+
|
package/TODO.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// TODO: Add index when component was registered but changed
|
package/bun.lock
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "bunsane",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"graphql": "^16.11.0",
|
|
8
|
+
"graphql-yoga": "^5.15.1",
|
|
9
|
+
"pino": "^9.9.0",
|
|
10
|
+
"pino-pretty": "^13.1.1",
|
|
11
|
+
"reflect-metadata": "^0.2.2",
|
|
12
|
+
"zod": "^4.1.5",
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "latest",
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "^5",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
"packages": {
|
|
23
|
+
"@envelop/core": ["@envelop/core@5.3.0", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@envelop/types": "^5.2.1", "@whatwg-node/promise-helpers": "^1.2.4", "tslib": "^2.5.0" } }, "sha512-xvUkOWXI8JsG2OOnqiI2tOkEc52wbmIqWORr7yGc8B8E53Oh1MMGGGck4mbR80s25LnHVzfNIiIlNkuDgZRuuA=="],
|
|
24
|
+
|
|
25
|
+
"@envelop/instrumentation": ["@envelop/instrumentation@1.0.0", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.2.1", "tslib": "^2.5.0" } }, "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw=="],
|
|
26
|
+
|
|
27
|
+
"@envelop/types": ["@envelop/types@5.2.1", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.5.0" } }, "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg=="],
|
|
28
|
+
|
|
29
|
+
"@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="],
|
|
30
|
+
|
|
31
|
+
"@graphql-tools/executor": ["@graphql-tools/executor@1.4.9", "", { "dependencies": { "@graphql-tools/utils": "^10.9.1", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-SAUlDT70JAvXeqV87gGzvDzUGofn39nvaVcVhNf12Dt+GfWHtNNO/RCn/Ea4VJaSLGzraUd41ObnN3i80EBU7w=="],
|
|
32
|
+
|
|
33
|
+
"@graphql-tools/merge": ["@graphql-tools/merge@9.1.1", "", { "dependencies": { "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-BJ5/7Y7GOhTuvzzO5tSBFL4NGr7PVqTJY3KeIDlVTT8YLcTXtBR+hlrC3uyEym7Ragn+zyWdHeJ9ev+nRX1X2w=="],
|
|
34
|
+
|
|
35
|
+
"@graphql-tools/schema": ["@graphql-tools/schema@10.0.25", "", { "dependencies": { "@graphql-tools/merge": "^9.1.1", "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw=="],
|
|
36
|
+
|
|
37
|
+
"@graphql-tools/utils": ["@graphql-tools/utils@10.9.1", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "dset": "^3.1.4", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw=="],
|
|
38
|
+
|
|
39
|
+
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
|
40
|
+
|
|
41
|
+
"@graphql-yoga/logger": ["@graphql-yoga/logger@2.0.1", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA=="],
|
|
42
|
+
|
|
43
|
+
"@graphql-yoga/subscription": ["@graphql-yoga/subscription@5.0.5", "", { "dependencies": { "@graphql-yoga/typed-event-target": "^3.0.2", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/events": "^0.1.0", "tslib": "^2.8.1" } }, "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw=="],
|
|
44
|
+
|
|
45
|
+
"@graphql-yoga/typed-event-target": ["@graphql-yoga/typed-event-target@3.0.2", "", { "dependencies": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.8.1" } }, "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA=="],
|
|
46
|
+
|
|
47
|
+
"@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="],
|
|
48
|
+
|
|
49
|
+
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
|
50
|
+
|
|
51
|
+
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
|
52
|
+
|
|
53
|
+
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
|
|
54
|
+
|
|
55
|
+
"@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="],
|
|
56
|
+
|
|
57
|
+
"@whatwg-node/events": ["@whatwg-node/events@0.1.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ=="],
|
|
58
|
+
|
|
59
|
+
"@whatwg-node/fetch": ["@whatwg-node/fetch@0.10.10", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.7.25", "urlpattern-polyfill": "^10.0.0" } }, "sha512-watz4i/Vv4HpoJ+GranJ7HH75Pf+OkPQ63NoVmru6Srgc8VezTArB00i/oQlnn0KWh14gM42F22Qcc9SU9mo/w=="],
|
|
60
|
+
|
|
61
|
+
"@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.7.25", "", { "dependencies": { "@fastify/busboy": "^3.1.1", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-szCTESNJV+Xd56zU6ShOi/JWROxE9IwCic8o5D9z5QECZloas6Ez5tUuKqXTAdu6fHFx1t6C+5gwj8smzOLjtg=="],
|
|
62
|
+
|
|
63
|
+
"@whatwg-node/promise-helpers": ["@whatwg-node/promise-helpers@1.3.2", "", { "dependencies": { "tslib": "^2.6.3" } }, "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA=="],
|
|
64
|
+
|
|
65
|
+
"@whatwg-node/server": ["@whatwg-node/server@0.10.12", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/fetch": "^0.10.10", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.6.3" } }, "sha512-MQIvvQyPvKGna586MzXhgwnEbGtbm7QtOgJ/KPd/tC70M/jbhd1xHdIQQbh3okBw+MrDF/EvaC2vB5oRC7QdlQ=="],
|
|
66
|
+
|
|
67
|
+
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
|
68
|
+
|
|
69
|
+
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
|
70
|
+
|
|
71
|
+
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
|
72
|
+
|
|
73
|
+
"cross-inspect": ["cross-inspect@1.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A=="],
|
|
74
|
+
|
|
75
|
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
|
76
|
+
|
|
77
|
+
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
|
|
78
|
+
|
|
79
|
+
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
|
80
|
+
|
|
81
|
+
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
|
82
|
+
|
|
83
|
+
"fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="],
|
|
84
|
+
|
|
85
|
+
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
|
|
86
|
+
|
|
87
|
+
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
|
|
88
|
+
|
|
89
|
+
"graphql": ["graphql@16.11.0", "", {}, "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="],
|
|
90
|
+
|
|
91
|
+
"graphql-yoga": ["graphql-yoga@5.15.1", "", { "dependencies": { "@envelop/core": "^5.3.0", "@envelop/instrumentation": "^1.0.0", "@graphql-tools/executor": "^1.4.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^10.6.2", "@graphql-yoga/logger": "^2.0.1", "@graphql-yoga/subscription": "^5.0.5", "@whatwg-node/fetch": "^0.10.6", "@whatwg-node/promise-helpers": "^1.2.4", "@whatwg-node/server": "^0.10.5", "dset": "^3.1.4", "lru-cache": "^10.0.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0" } }, "sha512-wCSnviFFGC4CF9lyeRNMW1p55xVWkMRLPu9iHYbBd8WCJEjduDTo3nh91sVktpbJdUQ6rxNBN6hhpTYMFZuMwg=="],
|
|
92
|
+
|
|
93
|
+
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
|
94
|
+
|
|
95
|
+
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
|
96
|
+
|
|
97
|
+
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
|
98
|
+
|
|
99
|
+
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
|
100
|
+
|
|
101
|
+
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
|
102
|
+
|
|
103
|
+
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
|
104
|
+
|
|
105
|
+
"pino": ["pino@9.9.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ=="],
|
|
106
|
+
|
|
107
|
+
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
|
|
108
|
+
|
|
109
|
+
"pino-pretty": ["pino-pretty@13.1.1", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA=="],
|
|
110
|
+
|
|
111
|
+
"pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="],
|
|
112
|
+
|
|
113
|
+
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
|
114
|
+
|
|
115
|
+
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
|
116
|
+
|
|
117
|
+
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
|
118
|
+
|
|
119
|
+
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
|
120
|
+
|
|
121
|
+
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
|
|
122
|
+
|
|
123
|
+
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
|
124
|
+
|
|
125
|
+
"secure-json-parse": ["secure-json-parse@4.0.0", "", {}, "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA=="],
|
|
126
|
+
|
|
127
|
+
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
|
|
128
|
+
|
|
129
|
+
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
|
130
|
+
|
|
131
|
+
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
|
|
132
|
+
|
|
133
|
+
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
|
|
134
|
+
|
|
135
|
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
136
|
+
|
|
137
|
+
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
|
138
|
+
|
|
139
|
+
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
|
140
|
+
|
|
141
|
+
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
|
|
142
|
+
|
|
143
|
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
|
144
|
+
|
|
145
|
+
"zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="],
|
|
146
|
+
}
|
|
147
|
+
}
|
package/core/App.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import ApplicationLifecycle, {ApplicationPhase} from "core/ApplicationLifecycle";
|
|
2
|
+
import { HasValidBaseTable, PrepareDatabase } from "database/DatabaseHelper";
|
|
3
|
+
import ComponentRegistry from "core/ComponentRegistry";
|
|
4
|
+
import { logger } from "core/Logger";
|
|
5
|
+
import { createYogaInstance } from "gql";
|
|
6
|
+
import ServiceRegistry from "service/ServiceRegistry";
|
|
7
|
+
|
|
8
|
+
export default class App {
|
|
9
|
+
private yoga: any;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.init();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async init() {
|
|
16
|
+
logger.trace(`Initializing App`);
|
|
17
|
+
ComponentRegistry.init();
|
|
18
|
+
ServiceRegistry.init();
|
|
19
|
+
if(ApplicationLifecycle.getCurrentPhase() === ApplicationPhase.DATABASE_INITIALIZING) {
|
|
20
|
+
if(!await HasValidBaseTable()) {
|
|
21
|
+
await PrepareDatabase();
|
|
22
|
+
}
|
|
23
|
+
logger.trace(`Database prepared...`);
|
|
24
|
+
ApplicationLifecycle.setPhase(ApplicationPhase.DATABASE_READY);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ApplicationLifecycle.addPhaseListener((event) => {
|
|
28
|
+
const phase = event.detail;
|
|
29
|
+
logger.info(`Application phase changed to: ${phase}`);
|
|
30
|
+
switch(phase) {
|
|
31
|
+
case ApplicationPhase.DATABASE_READY: {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case ApplicationPhase.COMPONENTS_READY: {
|
|
35
|
+
ApplicationLifecycle.setPhase(ApplicationPhase.SYSTEM_REGISTERING);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case ApplicationPhase.SYSTEM_READY: {
|
|
39
|
+
try {
|
|
40
|
+
const schema = ServiceRegistry.getSchema();
|
|
41
|
+
if (schema) {
|
|
42
|
+
this.yoga = createYogaInstance(schema);
|
|
43
|
+
} else {
|
|
44
|
+
this.yoga = createYogaInstance();
|
|
45
|
+
}
|
|
46
|
+
ApplicationLifecycle.setPhase(ApplicationPhase.APPLICATION_READY);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.error("Error during SYSTEM_READY phase:");
|
|
49
|
+
logger.error(error);
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case ApplicationPhase.APPLICATION_READY: {
|
|
54
|
+
if(process.env.NODE_ENV !== "test") {
|
|
55
|
+
this.start();
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
waitForAppReady(): Promise<void> {
|
|
64
|
+
return new Promise(resolve => {
|
|
65
|
+
const interval = setInterval(() => {
|
|
66
|
+
if (ApplicationLifecycle.getCurrentPhase() === ApplicationPhase.APPLICATION_READY) {
|
|
67
|
+
clearInterval(interval);
|
|
68
|
+
resolve();
|
|
69
|
+
}
|
|
70
|
+
}, 100);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async start() {
|
|
75
|
+
logger.info("Application Started");
|
|
76
|
+
const server = Bun.serve({
|
|
77
|
+
fetch: this.yoga
|
|
78
|
+
});
|
|
79
|
+
logger.info(`Server is running on ${new URL(this.yoga.graphqlEndpoint, `http://${server.hostname}:${server.port}`)}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
export enum ApplicationPhase {
|
|
3
|
+
DATABASE_INITIALIZING = "database_initializing",
|
|
4
|
+
DATABASE_READY = "database_ready",
|
|
5
|
+
COMPONENTS_REGISTERING = "components_registering",
|
|
6
|
+
COMPONENTS_READY = "components_ready",
|
|
7
|
+
SYSTEM_REGISTERING = "system_registering",
|
|
8
|
+
SYSTEM_READY = "system_ready",
|
|
9
|
+
APPLICATION_READY = "application_ready"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PhaseChangeEvent extends CustomEvent {
|
|
13
|
+
detail: ApplicationPhase;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApplicationLifecycle {
|
|
18
|
+
static #instance : ApplicationLifecycle;
|
|
19
|
+
constructor() {
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public static get instance(): ApplicationLifecycle {
|
|
23
|
+
if(ApplicationLifecycle.#instance === undefined) {
|
|
24
|
+
ApplicationLifecycle.#instance = new ApplicationLifecycle();
|
|
25
|
+
}
|
|
26
|
+
return ApplicationLifecycle.#instance;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private eventEmitter = new EventTarget();
|
|
30
|
+
private currentPhase = ApplicationPhase.DATABASE_INITIALIZING;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async waitForPhase(phase: ApplicationPhase): Promise<void> {
|
|
34
|
+
while (this.currentPhase !== phase) {
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
addPhaseListener(listener: (event: PhaseChangeEvent) => void) {
|
|
40
|
+
this.eventEmitter.addEventListener("phaseChanged", listener as EventListener);
|
|
41
|
+
return listener;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
removePhaseListener(listener: (event: PhaseChangeEvent) => void) {
|
|
45
|
+
this.eventEmitter.removeEventListener("phaseChanged", listener as EventListener);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setPhase(phase: ApplicationPhase) {
|
|
49
|
+
this.currentPhase = phase;
|
|
50
|
+
this.eventEmitter.dispatchEvent(new CustomEvent("phaseChanged", { detail: phase }));
|
|
51
|
+
}
|
|
52
|
+
getCurrentPhase() {
|
|
53
|
+
return this.currentPhase;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default ApplicationLifecycle.instance;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { generateTypeId, type BaseComponent } from "./Components";
|
|
2
|
+
import ApplicationLifecycle, { ApplicationPhase } from "./ApplicationLifecycle";
|
|
3
|
+
import { CreateComponentPartitionTable } from "database/DatabaseHelper";
|
|
4
|
+
import { GetSchema } from "database/DatabaseHelper";
|
|
5
|
+
import { logger as MainLogger } from "./Logger";
|
|
6
|
+
const logger = MainLogger.child({ scope: "ComponentRegistry" });
|
|
7
|
+
|
|
8
|
+
class ComponentRegistry {
|
|
9
|
+
static #instance: ComponentRegistry;
|
|
10
|
+
private componentQueue = new Map<string, new () => BaseComponent>();
|
|
11
|
+
private currentTables: string[] = [];
|
|
12
|
+
private componentsMap = new Map<string, string>();
|
|
13
|
+
private typeIdToCtor = new Map<string, new () => BaseComponent>();
|
|
14
|
+
private instantRegister: boolean = false;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public init() {
|
|
21
|
+
ApplicationLifecycle.addPhaseListener(async (event) => {
|
|
22
|
+
if(event.detail === ApplicationPhase.DATABASE_READY) {
|
|
23
|
+
logger.trace("Registering Components...");
|
|
24
|
+
ApplicationLifecycle.setPhase(ApplicationPhase.COMPONENTS_REGISTERING);
|
|
25
|
+
logger.trace(`Total Components to register: ${this.componentQueue.size}`);
|
|
26
|
+
await this.populateCurrentTables();
|
|
27
|
+
await this.registerAllComponents();
|
|
28
|
+
this.instantRegister = true;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static get instance(): ComponentRegistry {
|
|
34
|
+
if (!this.#instance) {
|
|
35
|
+
this.#instance = new ComponentRegistry();
|
|
36
|
+
}
|
|
37
|
+
return this.#instance;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async populateCurrentTables() {
|
|
41
|
+
try {
|
|
42
|
+
this.currentTables = await GetSchema();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.warn(`Failed to populate current tables: ${error}`);
|
|
45
|
+
this.currentTables = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
define(
|
|
50
|
+
name: string,
|
|
51
|
+
ctor: new () => BaseComponent
|
|
52
|
+
) {
|
|
53
|
+
if(!this.instantRegister) {
|
|
54
|
+
if(!this.componentQueue.has(name)) {
|
|
55
|
+
this.componentQueue.set(name, ctor);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if(this.instantRegister) {
|
|
60
|
+
if(this.componentsMap.has(name)) {
|
|
61
|
+
logger.trace(`Component already registered: ${name}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.register(name, generateTypeId(name), ctor);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
componentSize() {
|
|
69
|
+
return this.componentQueue.size;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isComponentReady(name: string): boolean {
|
|
73
|
+
return this.componentsMap.has(name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getComponentId(name: string) {
|
|
77
|
+
return this.componentsMap.get(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getConstructor(typeId: string) {
|
|
81
|
+
return this.typeIdToCtor.get(typeId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async registerAllComponents(): Promise<void> {
|
|
85
|
+
logger.trace(`Registering all components`);
|
|
86
|
+
for(const [name, ctor] of this.componentQueue) {
|
|
87
|
+
const typeId = generateTypeId(name);
|
|
88
|
+
await this.register(name, typeId, ctor);
|
|
89
|
+
}
|
|
90
|
+
ApplicationLifecycle.setPhase(ApplicationPhase.COMPONENTS_READY);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
register(name: string, typeid: string, ctor: new () => BaseComponent) {
|
|
94
|
+
return new Promise<boolean>(async resolve => {
|
|
95
|
+
const partitionTableName = `components_${this.sluggifyName(name)}`;
|
|
96
|
+
await this.populateCurrentTables();
|
|
97
|
+
if (!this.currentTables.includes(partitionTableName)) {
|
|
98
|
+
logger.trace(`Partition table ${partitionTableName} does not exist. Creating... name: ${name}, typeId: ${typeid}`);
|
|
99
|
+
const instance = new ctor();
|
|
100
|
+
const indexedProps = instance.indexedProperties();
|
|
101
|
+
await CreateComponentPartitionTable(name, typeid, indexedProps);
|
|
102
|
+
await this.populateCurrentTables();
|
|
103
|
+
}
|
|
104
|
+
this.componentsMap.set(name, typeid);
|
|
105
|
+
this.typeIdToCtor.set(typeid, ctor);
|
|
106
|
+
resolve(true);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
private sluggifyName(name: string) {
|
|
112
|
+
return name.toLowerCase().replace(/\s+/g, '_');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default ComponentRegistry.instance;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
import { logger as MainLogger } from "./Logger";
|
|
4
|
+
import ComponentRegistry from "./ComponentRegistry";
|
|
5
|
+
import { uuidv7 } from 'utils/uuid';
|
|
6
|
+
const logger = MainLogger.child({ scope: "Components" });
|
|
7
|
+
|
|
8
|
+
export function generateTypeId(name: string): string {
|
|
9
|
+
return createHash('sha256').update(name).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CompData(options?: { indexed?: boolean }) {
|
|
13
|
+
return Reflect.metadata("compData", { isData: true, indexed: options?.indexed ?? false });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Type helper to extract only data properties (excludes methods and private properties)
|
|
17
|
+
export type ComponentDataType<T extends BaseComponent> = {
|
|
18
|
+
[K in keyof T as T[K] extends Function ? never :
|
|
19
|
+
K extends `_${string}` ? never :
|
|
20
|
+
K extends 'id' | 'getTypeID' | 'properties' | 'data' | 'save' | 'insert' | 'update' ? never :
|
|
21
|
+
K]: T[K];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function Component(target: any) {
|
|
25
|
+
ComponentRegistry.define(target.name, target);
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class BaseComponent {
|
|
30
|
+
public id: string = "";
|
|
31
|
+
protected _comp_name: string = "";
|
|
32
|
+
protected _typeId: string = "";
|
|
33
|
+
protected _persisted: boolean = false;
|
|
34
|
+
protected _dirty: boolean = false;
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
this._comp_name = this.constructor.name;
|
|
38
|
+
this._typeId = ComponentRegistry.getComponentId(this._comp_name) || generateTypeId(this._comp_name);
|
|
39
|
+
this._dirty = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getTypeID(): string {
|
|
43
|
+
return this._typeId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
properties(): string[] {
|
|
47
|
+
return Object.keys(this).filter(prop => {
|
|
48
|
+
const meta = Reflect.getMetadata("compData", this, prop);
|
|
49
|
+
return meta && meta.isData;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get data for this component
|
|
55
|
+
* @returns Object containing only properties marked with @CompData decorator
|
|
56
|
+
*/
|
|
57
|
+
data<T extends this>(): ComponentDataType<T> {
|
|
58
|
+
const data: Record<string, any> = {};
|
|
59
|
+
this.properties().forEach((prop: string) => {
|
|
60
|
+
data[prop] = (this as any)[prop];
|
|
61
|
+
});
|
|
62
|
+
return data as ComponentDataType<T>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async save(trx: Bun.SQL, entity_id: string) {
|
|
66
|
+
logger.trace(`Saving component ${this._comp_name} for entity ${entity_id}`);
|
|
67
|
+
logger.trace(`Checking is Component can be saved (is registered)`);
|
|
68
|
+
await new Promise(resolve => {
|
|
69
|
+
if(ComponentRegistry.isComponentReady(this._comp_name)) {
|
|
70
|
+
resolve(true);
|
|
71
|
+
} else {
|
|
72
|
+
const interval = setInterval(() => {
|
|
73
|
+
if (ComponentRegistry.isComponentReady(this._comp_name)) {
|
|
74
|
+
clearInterval(interval);
|
|
75
|
+
resolve(true);
|
|
76
|
+
}
|
|
77
|
+
}, 100);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
logger.trace(`Component Registered`);
|
|
81
|
+
if(this._persisted) {
|
|
82
|
+
await this.update(trx);
|
|
83
|
+
} else {
|
|
84
|
+
await this.insert(trx, entity_id);
|
|
85
|
+
this._persisted = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async insert(trx: Bun.SQL, entity_id: string) {
|
|
90
|
+
if(this.id === "") {
|
|
91
|
+
this.id = uuidv7();
|
|
92
|
+
}
|
|
93
|
+
await trx`INSERT INTO components
|
|
94
|
+
(id, entity_id, name, type_id, data)
|
|
95
|
+
VALUES (${this.id}, ${entity_id}, ${this._comp_name}, ${this._typeId}, ${this.data()})`
|
|
96
|
+
await trx`INSERT INTO entity_components (entity_id, type_id) VALUES (${entity_id}, ${this._typeId}) ON CONFLICT DO NOTHING`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async update(trx: Bun.SQL) {
|
|
100
|
+
if(this.id === "") {
|
|
101
|
+
throw new Error("Component must have an ID to be updated");
|
|
102
|
+
}
|
|
103
|
+
await trx`UPDATE components SET data = ${this.data()} WHERE id = ${this.id}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public setPersisted(persisted: boolean) {
|
|
107
|
+
this._persisted = persisted;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public setDirty(dirty: boolean) {
|
|
111
|
+
this._dirty = dirty;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
indexedProperties(): string[] {
|
|
115
|
+
return Object.keys(this).filter(prop => {
|
|
116
|
+
const meta = Reflect.getMetadata("compData", this, prop);
|
|
117
|
+
return meta && meta.isData && meta.indexed;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ComponentGetter<T extends BaseComponent> = Pick<T, "properties" | "id"> & {
|
|
123
|
+
data(): ComponentDataType<T>;
|
|
124
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
2
|
+
const originalMethod = descriptor.value;
|
|
3
|
+
|
|
4
|
+
descriptor.value = function(...args: any[]) {
|
|
5
|
+
console.log(`Calling ${propertyKey} with:`, args);
|
|
6
|
+
return originalMethod.apply(this, args);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function timed(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
11
|
+
const originalMethod = descriptor.value;
|
|
12
|
+
|
|
13
|
+
descriptor.value = async function(...args: any[]) {
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
const result = await originalMethod.apply(this, args);
|
|
16
|
+
const end = performance.now();
|
|
17
|
+
console.log(`Execution time for ${propertyKey}: ${end - start} ms`);
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
}
|