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 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
+ }