bunsane 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/TODO.md +1 -1
- package/bun.lock +156 -150
- package/core/App.ts +188 -31
- package/core/ArcheType.ts +1044 -26
- package/core/ComponentRegistry.ts +172 -29
- package/core/Components.ts +102 -24
- package/core/Decorators.ts +0 -1
- package/core/Entity.ts +55 -7
- package/core/EntityInterface.ts +4 -0
- package/core/EntityManager.ts +4 -4
- package/core/Query.ts +169 -3
- package/core/RequestLoaders.ts +101 -12
- package/core/SchedulerManager.ts +3 -4
- package/core/metadata/definitions/ArcheType.ts +9 -0
- package/core/metadata/definitions/Component.ts +16 -0
- package/core/metadata/definitions/gqlObject.ts +10 -0
- package/core/metadata/getMetadataStorage.ts +14 -0
- package/core/metadata/index.ts +17 -0
- package/core/metadata/metadata-storage.ts +81 -0
- package/database/DatabaseHelper.ts +22 -20
- package/database/index.ts +6 -1
- package/database/sqlHelpers.ts +0 -2
- package/gql/ArchetypeOperations.ts +281 -0
- package/gql/Generator.ts +252 -62
- package/gql/helpers.ts +5 -5
- package/gql/index.ts +19 -17
- package/gql/types.ts +58 -11
- package/index.ts +93 -82
- package/package.json +39 -37
- package/plugins/index.ts +13 -0
- package/scheduler/index.ts +87 -0
- package/service/Service.ts +4 -0
- package/service/ServiceRegistry.ts +5 -1
- package/service/index.ts +1 -1
- package/swagger/decorators.ts +65 -0
- package/swagger/generator.ts +100 -0
- package/swagger/index.ts +2 -0
- package/tests/bench/insert.bench.ts +1 -0
- package/tests/bench/relations.bench.ts +1 -0
- package/tests/bench/sorting.bench.ts +1 -0
- package/tests/component-hooks-simple.test.ts +117 -0
- package/tests/component-hooks.test.ts +83 -31
- package/tests/component.test.ts +1 -0
- package/tests/hooks.test.ts +1 -0
- package/tests/query.test.ts +46 -4
- package/tests/relations.test.ts +1 -0
- package/types/app.types.ts +0 -0
- package/upload/index.ts +0 -2
- package/core/processors/ImageProcessor.ts +0 -423
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import { generateTypeId, type BaseComponent } from "./Components";
|
|
2
2
|
import ApplicationLifecycle, { ApplicationPhase } from "./ApplicationLifecycle";
|
|
3
|
-
import { CreateComponentPartitionTable, UpdateComponentIndexes } from "database/DatabaseHelper";
|
|
3
|
+
import { CreateComponentPartitionTable, GenerateTableName, UpdateComponentIndexes } from "database/DatabaseHelper";
|
|
4
4
|
import { GetSchema } from "database/DatabaseHelper";
|
|
5
5
|
import { logger as MainLogger } from "./Logger";
|
|
6
|
+
import { getMetadataStorage } from "./metadata";
|
|
7
|
+
import { registerDecoratedHooks } from "./decorators/EntityHooks";
|
|
8
|
+
import ServiceRegistry from "service/ServiceRegistry";
|
|
6
9
|
const logger = MainLogger.child({ scope: "ComponentRegistry" });
|
|
7
10
|
|
|
11
|
+
type ComponentConstructor = new () => BaseComponent;
|
|
12
|
+
|
|
13
|
+
export type { ComponentConstructor };
|
|
14
|
+
|
|
8
15
|
class ComponentRegistry {
|
|
9
16
|
static #instance: ComponentRegistry;
|
|
10
|
-
private componentQueue = new Map<string,
|
|
17
|
+
private componentQueue = new Map<string, ComponentConstructor>();
|
|
11
18
|
private currentTables: string[] = [];
|
|
12
19
|
private componentsMap = new Map<string, string>();
|
|
13
|
-
private typeIdToCtor = new Map<string,
|
|
20
|
+
private typeIdToCtor = new Map<string, ComponentConstructor>();
|
|
14
21
|
private instantRegister: boolean = false;
|
|
22
|
+
private readinessPromises = new Map<string, Promise<void>>();
|
|
23
|
+
private readinessResolvers = new Map<string, () => void>();
|
|
24
|
+
private componentsRegistered: boolean = false;
|
|
15
25
|
|
|
16
26
|
constructor() {
|
|
17
27
|
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
public init() {
|
|
21
|
-
|
|
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
|
+
// Listener removed to make component registration sequential
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
public static get instance(): ComponentRegistry {
|
|
@@ -48,11 +49,14 @@ class ComponentRegistry {
|
|
|
48
49
|
|
|
49
50
|
define(
|
|
50
51
|
name: string,
|
|
51
|
-
ctor:
|
|
52
|
+
ctor: ComponentConstructor
|
|
52
53
|
) {
|
|
53
54
|
if(!this.instantRegister) {
|
|
54
55
|
if(!this.componentQueue.has(name)) {
|
|
55
56
|
this.componentQueue.set(name, ctor);
|
|
57
|
+
this.readinessPromises.set(name, new Promise<void>(resolve => {
|
|
58
|
+
this.readinessResolvers.set(name, resolve);
|
|
59
|
+
}));
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
}
|
|
@@ -61,7 +65,10 @@ class ComponentRegistry {
|
|
|
61
65
|
logger.trace(`Component already registered: ${name}`);
|
|
62
66
|
return;
|
|
63
67
|
}
|
|
64
|
-
this.register(name, generateTypeId(name), ctor)
|
|
68
|
+
this.register(name, generateTypeId(name), ctor).then(() => {
|
|
69
|
+
const resolve = this.readinessResolvers.get(name);
|
|
70
|
+
if(resolve) resolve();
|
|
71
|
+
});
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -73,6 +80,36 @@ class ComponentRegistry {
|
|
|
73
80
|
return this.componentsMap.has(name);
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
async getReadyPromise(name: string): Promise<void> {
|
|
84
|
+
if (this.isComponentReady(name)) {
|
|
85
|
+
return Promise.resolve();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Ensure components are registered before trying to find the component
|
|
89
|
+
await this.ensureComponentsRegistered();
|
|
90
|
+
|
|
91
|
+
if (this.isComponentReady(name)) {
|
|
92
|
+
return Promise.resolve();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const storage = getMetadataStorage();
|
|
96
|
+
const component = storage.components.find(c => c.name === name);
|
|
97
|
+
if (component) {
|
|
98
|
+
// Component exists in metadata but not registered yet, register it
|
|
99
|
+
return this.registerComponentFromMetadata(component);
|
|
100
|
+
}
|
|
101
|
+
// Check if component is in the queue (defined but not registered)
|
|
102
|
+
if (this.componentQueue.has(name)) {
|
|
103
|
+
const promise = this.readinessPromises.get(name);
|
|
104
|
+
if (promise) {
|
|
105
|
+
return promise;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Component not found anywhere, try to register it dynamically
|
|
109
|
+
// This handles test components that are decorated but not imported in main app
|
|
110
|
+
return this.registerComponentDynamically(name);
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
getComponentId(name: string) {
|
|
77
114
|
return this.componentsMap.get(name);
|
|
78
115
|
}
|
|
@@ -81,36 +118,142 @@ class ComponentRegistry {
|
|
|
81
118
|
return this.typeIdToCtor.get(typeId);
|
|
82
119
|
}
|
|
83
120
|
|
|
121
|
+
// TODO: OLD LOGIC Remove if not needed
|
|
122
|
+
// async registerAllComponents(): Promise<void> {
|
|
123
|
+
// logger.trace(`Registering all components`);
|
|
124
|
+
// for(const [name, ctor] of this.componentQueue) {
|
|
125
|
+
// const typeId = generateTypeId(name);
|
|
126
|
+
// await this.register(name, typeId, ctor);
|
|
127
|
+
// }
|
|
128
|
+
// ApplicationLifecycle.setPhase(ApplicationPhase.COMPONENTS_READY);
|
|
129
|
+
// // Resolve all pending readiness promises
|
|
130
|
+
// for(const [name] of this.componentQueue) {
|
|
131
|
+
// const resolve = this.readinessResolvers.get(name);
|
|
132
|
+
// if(resolve) resolve();
|
|
133
|
+
// }
|
|
134
|
+
// }
|
|
135
|
+
|
|
84
136
|
async registerAllComponents(): Promise<void> {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const typeId = generateTypeId(name);
|
|
88
|
-
await this.register(name, typeId, ctor);
|
|
137
|
+
if (this.componentsRegistered) {
|
|
138
|
+
return; // Already registered
|
|
89
139
|
}
|
|
140
|
+
|
|
141
|
+
logger.trace("Registering Components...");
|
|
142
|
+
ApplicationLifecycle.setPhase(ApplicationPhase.COMPONENTS_REGISTERING);
|
|
143
|
+
|
|
144
|
+
await this.populateCurrentTables();
|
|
145
|
+
const storage = getMetadataStorage();
|
|
146
|
+
const promises = storage.components.map(async metadata => {
|
|
147
|
+
const { name, target: ctor, typeId } = metadata;
|
|
148
|
+
if(this.componentsMap.has(name)) {
|
|
149
|
+
logger.trace(`Component already registered: ${name}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.readinessPromises.set(name, new Promise<void>(resolve => {
|
|
153
|
+
this.readinessResolvers.set(name, resolve);
|
|
154
|
+
}));
|
|
155
|
+
await this.register(name, typeId, ctor as ComponentConstructor);
|
|
156
|
+
const resolve = this.readinessResolvers.get(name);
|
|
157
|
+
if(resolve) resolve();
|
|
158
|
+
});
|
|
159
|
+
await Promise.all(promises);
|
|
160
|
+
this.componentsRegistered = true;
|
|
161
|
+
|
|
162
|
+
// Handle component-related setup that was previously in App.init()
|
|
163
|
+
await this.setupComponentFeatures();
|
|
164
|
+
|
|
90
165
|
ApplicationLifecycle.setPhase(ApplicationPhase.COMPONENTS_READY);
|
|
91
166
|
}
|
|
92
167
|
|
|
93
|
-
register(name: string, typeid: string, ctor:
|
|
168
|
+
register(name: string, typeid: string, ctor: ComponentConstructor) {
|
|
94
169
|
return new Promise<boolean>(async resolve => {
|
|
95
|
-
const partitionTableName =
|
|
96
|
-
await this.populateCurrentTables();
|
|
97
|
-
const instance = new ctor();
|
|
98
|
-
const indexedProps = instance.indexedProperties();
|
|
170
|
+
const partitionTableName = GenerateTableName(name);
|
|
171
|
+
// await this.populateCurrentTables();
|
|
172
|
+
// const instance = new ctor();
|
|
173
|
+
// const indexedProps = instance.indexedProperties();
|
|
99
174
|
if (!this.currentTables.includes(partitionTableName)) {
|
|
100
175
|
logger.trace(`Partition table ${partitionTableName} does not exist. Creating... name: ${name}, typeId: ${typeid}`);
|
|
101
|
-
await CreateComponentPartitionTable(name, typeid, indexedProps);
|
|
102
|
-
await
|
|
176
|
+
// await CreateComponentPartitionTable(name, typeid, indexedProps); // TODO: OLD Logic with indexedProps, remove if not needed
|
|
177
|
+
await CreateComponentPartitionTable(name, typeid);
|
|
178
|
+
// await this.populateCurrentTables();
|
|
103
179
|
}
|
|
104
|
-
await UpdateComponentIndexes(partitionTableName, indexedProps);
|
|
180
|
+
// await UpdateComponentIndexes(partitionTableName, indexedProps); // TODO: OLD Logic with indexedProps, remove if not needed
|
|
105
181
|
this.componentsMap.set(name, typeid);
|
|
106
182
|
this.typeIdToCtor.set(typeid, ctor);
|
|
107
183
|
resolve(true);
|
|
108
184
|
});
|
|
109
185
|
}
|
|
110
186
|
|
|
187
|
+
private async registerComponentFromMetadata(component: any): Promise<void> {
|
|
188
|
+
const { name, target: ctor, typeId } = component;
|
|
189
|
+
if (this.componentsMap.has(name)) {
|
|
190
|
+
return; // Already registered
|
|
191
|
+
}
|
|
192
|
+
this.readinessPromises.set(name, new Promise<void>(resolve => {
|
|
193
|
+
this.readinessResolvers.set(name, resolve);
|
|
194
|
+
}));
|
|
195
|
+
await this.register(name, typeId, ctor as ComponentConstructor);
|
|
196
|
+
const resolve = this.readinessResolvers.get(name);
|
|
197
|
+
if (resolve) resolve();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async registerComponentDynamically(name: string): Promise<void> {
|
|
201
|
+
// Try to find the component in global metadata storage
|
|
202
|
+
const storage = getMetadataStorage();
|
|
203
|
+
const component = storage.components.find(c => c.name === name);
|
|
204
|
+
if (component) {
|
|
205
|
+
return this.registerComponentFromMetadata(component);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// If still not found, this is an error - component was never decorated
|
|
209
|
+
throw new Error(`Component ${name} not found in metadata storage. Make sure it's decorated with @Component`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getComponents() {
|
|
213
|
+
// returns array of { name, ctor }
|
|
214
|
+
const components: { name: string, ctor: ComponentConstructor }[] = [];
|
|
215
|
+
for (const [name, typeid] of this.componentsMap) {
|
|
216
|
+
const ctor = this.typeIdToCtor.get(typeid);
|
|
217
|
+
if(ctor) {
|
|
218
|
+
components.push({ name, ctor });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return components;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async ensureComponentsRegistered(): Promise<void> {
|
|
225
|
+
if (!this.componentsRegistered) {
|
|
226
|
+
// If components haven't been registered yet, register them now
|
|
227
|
+
// This handles cases where components are needed before DATABASE_READY phase
|
|
228
|
+
logger.trace("Ensuring components are registered...");
|
|
229
|
+
await this.registerAllComponents();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
111
232
|
|
|
112
|
-
private
|
|
113
|
-
|
|
233
|
+
private async setupComponentFeatures(): Promise<void> {
|
|
234
|
+
const components = this.getComponents();
|
|
235
|
+
|
|
236
|
+
// Update component indexes for components that have indexed properties
|
|
237
|
+
for(const {name, ctor} of components) {
|
|
238
|
+
const instance = new ctor();
|
|
239
|
+
if(instance.indexedProperties().length > 0) {
|
|
240
|
+
const table_name = GenerateTableName(name);
|
|
241
|
+
UpdateComponentIndexes(table_name, instance.indexedProperties());
|
|
242
|
+
logger.trace(`Updated indexes for component: ${name}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Automatically register decorated hooks for all services
|
|
247
|
+
const services = ServiceRegistry.getServices();
|
|
248
|
+
for (const service of services) {
|
|
249
|
+
try {
|
|
250
|
+
registerDecoratedHooks(service);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger.warn(`Failed to register hooks for service ${service.constructor.name}`);
|
|
253
|
+
logger.warn(error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
logger.info(`Registered hooks for ${services.length} services`);
|
|
114
257
|
}
|
|
115
258
|
}
|
|
116
259
|
|
package/core/Components.ts
CHANGED
|
@@ -3,16 +3,90 @@ import "reflect-metadata";
|
|
|
3
3
|
import { logger as MainLogger } from "./Logger";
|
|
4
4
|
import ComponentRegistry from "./ComponentRegistry";
|
|
5
5
|
import { uuidv7 } from 'utils/uuid';
|
|
6
|
+
import { getMetadataStorage } from './metadata';
|
|
6
7
|
const logger = MainLogger.child({ scope: "Components" });
|
|
7
8
|
|
|
8
9
|
export function generateTypeId(name: string): string {
|
|
9
10
|
return createHash('sha256').update(name).digest('hex');
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
const primitiveTypes = [String, Number, Boolean, Symbol, BigInt];
|
|
14
|
+
|
|
15
|
+
//TODO: Continue here
|
|
12
16
|
export function CompData(options?: { indexed?: boolean }) {
|
|
13
|
-
return
|
|
17
|
+
return (target: any, propertyKey: string) => {
|
|
18
|
+
const storage = getMetadataStorage();
|
|
19
|
+
const typeId = storage.getComponentId(target.constructor.name);
|
|
20
|
+
const propType = Reflect.getMetadata("design:type", target, propertyKey);
|
|
21
|
+
let isEnum = !!(Reflect.getMetadata("isEnum", propType));
|
|
22
|
+
if (propType.name === 'ServiceType') isEnum = true;
|
|
23
|
+
// console.log(`Property ${propertyKey} type:`, propType?.name);
|
|
24
|
+
// console.log(`Is Enum:`, isEnum);
|
|
25
|
+
let enumValues: string[] | undefined = undefined;
|
|
26
|
+
let enumKeys: string[] | undefined = undefined;
|
|
27
|
+
if(isEnum) {
|
|
28
|
+
const metaEnumValues = Reflect.getMetadata("__enumValues", propType);
|
|
29
|
+
const metaEnumKeys = Reflect.getMetadata("__enumKeys", propType);
|
|
30
|
+
|
|
31
|
+
if (metaEnumValues && metaEnumKeys) {
|
|
32
|
+
enumValues = metaEnumValues;
|
|
33
|
+
enumKeys = metaEnumKeys;
|
|
34
|
+
} else {
|
|
35
|
+
const staticKeys = Object.getOwnPropertyNames(propType).filter(key =>
|
|
36
|
+
key !== 'prototype' &&
|
|
37
|
+
key !== 'length' &&
|
|
38
|
+
key !== 'name' &&
|
|
39
|
+
key !== 'isEnum' &&
|
|
40
|
+
key !== '__enumValues' &&
|
|
41
|
+
key !== '__enumKeys' &&
|
|
42
|
+
typeof propType[key] !== 'function' &&
|
|
43
|
+
typeof propType[key] !== 'boolean'
|
|
44
|
+
);
|
|
45
|
+
if (staticKeys.length > 0) {
|
|
46
|
+
enumValues = staticKeys.map(key => propType[key]);
|
|
47
|
+
enumKeys = staticKeys;
|
|
48
|
+
} else {
|
|
49
|
+
// Fallback for numeric enums
|
|
50
|
+
enumValues = Object.keys(propType).filter(key => !isNaN(Number(key))).map(key => propType[key]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (propType.name === 'ServiceType' && (!enumValues || enumValues.length === 0)) {
|
|
55
|
+
enumValues = ["jek", "car", "food", "package"];
|
|
56
|
+
enumKeys = ["BIKE", "CAR", "FOOD", "PACKAGE"];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
storage.collectComponentPropertyMetadata({
|
|
61
|
+
component_id: typeId,
|
|
62
|
+
propertyKey: propertyKey,
|
|
63
|
+
propertyType: propType,
|
|
64
|
+
indexed: options?.indexed ?? false,
|
|
65
|
+
isPrimitive: primitiveTypes.includes(propType),
|
|
66
|
+
isEnum: isEnum,
|
|
67
|
+
enumValues: enumValues,
|
|
68
|
+
enumKeys: enumKeys,
|
|
69
|
+
})
|
|
70
|
+
// Reflect.metadata("compData", { isData: true, indexed: options?.indexed ?? false })(target, propertyKey);
|
|
71
|
+
};
|
|
14
72
|
}
|
|
15
73
|
|
|
74
|
+
// TODO: Component Property Casting
|
|
75
|
+
// export enum CompCastingType {
|
|
76
|
+
// STRING = "string",
|
|
77
|
+
// NUMBER = "number",
|
|
78
|
+
// BOOLEAN = "boolean",
|
|
79
|
+
// DATE = "date",
|
|
80
|
+
// }
|
|
81
|
+
// /**
|
|
82
|
+
// * Cast property to specific type when loading from database
|
|
83
|
+
// * @param type Casting type for the property
|
|
84
|
+
// * @returns
|
|
85
|
+
// */
|
|
86
|
+
// export function Cast(type: CompCastingType) {
|
|
87
|
+
// return Reflect.metadata("compCast", { type });
|
|
88
|
+
// }
|
|
89
|
+
|
|
16
90
|
// Type helper to extract only data properties (excludes methods and private properties)
|
|
17
91
|
export type ComponentDataType<T extends BaseComponent> = {
|
|
18
92
|
[K in keyof T as T[K] extends Function ? never :
|
|
@@ -21,8 +95,17 @@ export type ComponentDataType<T extends BaseComponent> = {
|
|
|
21
95
|
K]: T[K];
|
|
22
96
|
};
|
|
23
97
|
|
|
24
|
-
export function Component(target:
|
|
25
|
-
|
|
98
|
+
export function Component<T extends new () => BaseComponent>(target: T): T {
|
|
99
|
+
const storage = getMetadataStorage();
|
|
100
|
+
const typeId = storage.getComponentId(target.name);
|
|
101
|
+
const properties = storage.getComponentProperties(typeId);
|
|
102
|
+
// console.log(`Component decorator applied to ${target.name} with typeId ${typeId} and properties:`, properties);
|
|
103
|
+
storage.collectComponentMetadata({
|
|
104
|
+
name: target.name,
|
|
105
|
+
typeId: typeId,
|
|
106
|
+
target: target,
|
|
107
|
+
});
|
|
108
|
+
// ComponentRegistry.define(target.name, target);
|
|
26
109
|
return target;
|
|
27
110
|
}
|
|
28
111
|
|
|
@@ -35,7 +118,8 @@ export class BaseComponent {
|
|
|
35
118
|
|
|
36
119
|
constructor() {
|
|
37
120
|
this._comp_name = this.constructor.name;
|
|
38
|
-
|
|
121
|
+
const storage = getMetadataStorage();
|
|
122
|
+
this._typeId = storage.getComponentId(this._comp_name);
|
|
39
123
|
this._dirty = false;
|
|
40
124
|
}
|
|
41
125
|
|
|
@@ -44,10 +128,15 @@ export class BaseComponent {
|
|
|
44
128
|
}
|
|
45
129
|
|
|
46
130
|
properties(): string[] {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
131
|
+
const storage = getMetadataStorage();
|
|
132
|
+
const props = storage.componentProperties.get(this._typeId);
|
|
133
|
+
if(!props) return [];
|
|
134
|
+
return props.map(p => p.propertyKey);
|
|
135
|
+
//
|
|
136
|
+
// return Object.keys(this).filter(prop => {
|
|
137
|
+
// const meta = Reflect.getMetadata("compData", Object.getPrototypeOf(this), prop);
|
|
138
|
+
// return meta && meta.isData;
|
|
139
|
+
// });
|
|
51
140
|
}
|
|
52
141
|
|
|
53
142
|
/**
|
|
@@ -65,18 +154,7 @@ export class BaseComponent {
|
|
|
65
154
|
async save(trx: Bun.SQL, entity_id: string) {
|
|
66
155
|
logger.trace(`Saving component ${this._comp_name} for entity ${entity_id}`);
|
|
67
156
|
logger.trace(`Checking is Component can be saved (is registered)`);
|
|
68
|
-
await
|
|
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
|
-
});
|
|
157
|
+
await ComponentRegistry.getReadyPromise(this._comp_name);
|
|
80
158
|
logger.trace(`Component Registered`);
|
|
81
159
|
if(this._persisted) {
|
|
82
160
|
await this.update(trx);
|
|
@@ -112,10 +190,10 @@ export class BaseComponent {
|
|
|
112
190
|
}
|
|
113
191
|
|
|
114
192
|
indexedProperties(): string[] {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
193
|
+
const storage = getMetadataStorage();
|
|
194
|
+
const props = storage.componentProperties.get(this._typeId);
|
|
195
|
+
if(!props) return [];
|
|
196
|
+
return props.filter(p => p.indexed).map(p => p.propertyKey);
|
|
119
197
|
}
|
|
120
198
|
}
|
|
121
199
|
|
package/core/Decorators.ts
CHANGED
|
@@ -3,7 +3,6 @@ export function log(target: any, propertyKey: string, descriptor: PropertyDescri
|
|
|
3
3
|
const originalMethod = descriptor.value;
|
|
4
4
|
|
|
5
5
|
descriptor.value = function(...args: any[]) {
|
|
6
|
-
console.log(`Calling ${propertyKey} with:`, args);
|
|
7
6
|
return originalMethod.apply(this, args);
|
|
8
7
|
};
|
|
9
8
|
}
|
package/core/Entity.ts
CHANGED
|
@@ -5,12 +5,13 @@ import EntityManager from "./EntityManager";
|
|
|
5
5
|
import ComponentRegistry from "./ComponentRegistry";
|
|
6
6
|
import { uuidv7 } from "utils/uuid";
|
|
7
7
|
import { sql } from "bun";
|
|
8
|
-
import Query from "./Query";
|
|
8
|
+
// import Query from "./Query"; // Lazy import to avoid cycle
|
|
9
9
|
import { timed } from "./Decorators";
|
|
10
10
|
import EntityHookManager from "./EntityHookManager";
|
|
11
11
|
import { EntityCreatedEvent, EntityUpdatedEvent, EntityDeletedEvent, ComponentAddedEvent, ComponentUpdatedEvent, ComponentRemovedEvent } from "./events/EntityLifecycleEvents";
|
|
12
|
+
import type { IEntity } from "./EntityInterface";
|
|
12
13
|
|
|
13
|
-
export class Entity {
|
|
14
|
+
export class Entity implements IEntity {
|
|
14
15
|
id: string;
|
|
15
16
|
public _persisted: boolean = false;
|
|
16
17
|
private components: Map<string, BaseComponent> = new Map<string, BaseComponent>();
|
|
@@ -39,11 +40,15 @@ export class Entity {
|
|
|
39
40
|
* Adds a new component to the entity.
|
|
40
41
|
* Use like: entity.add(Component, { value: "Test" })
|
|
41
42
|
*/
|
|
42
|
-
public add<T extends BaseComponent>(ctor: new (...args: any[]) => T, data
|
|
43
|
+
public add<T extends BaseComponent>(ctor: new (...args: any[]) => T, data?: Partial<ComponentDataType<T>>): this {
|
|
43
44
|
const instance = new ctor();
|
|
44
|
-
|
|
45
|
+
if (data) {
|
|
46
|
+
Object.assign(instance, data);
|
|
47
|
+
} else {
|
|
48
|
+
Object.assign(instance, {});
|
|
49
|
+
}
|
|
45
50
|
this.addComponent(instance);
|
|
46
|
-
|
|
51
|
+
this._dirty = true;
|
|
47
52
|
// Fire component added event
|
|
48
53
|
try {
|
|
49
54
|
EntityHookManager.executeHooks(new ComponentAddedEvent(this, instance));
|
|
@@ -84,6 +89,7 @@ export class Entity {
|
|
|
84
89
|
} else {
|
|
85
90
|
// Add new component
|
|
86
91
|
this.add(ctor, data);
|
|
92
|
+
this._dirty = true;
|
|
87
93
|
// Note: add() already fires ComponentAddedEvent, so we don't need to fire it again
|
|
88
94
|
}
|
|
89
95
|
return this;
|
|
@@ -155,6 +161,40 @@ export class Entity {
|
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Get a component from the entity.
|
|
166
|
+
* @param ctor Constructor of the component to fetch
|
|
167
|
+
* @returns Component instance or null if not found
|
|
168
|
+
*/
|
|
169
|
+
public async getComponent<T extends BaseComponent>(ctor: new (...args: any[]) => T): Promise<T | null> {
|
|
170
|
+
const comp = Array.from(this.components.values()).find(comp => comp instanceof ctor) as T | undefined;
|
|
171
|
+
if(typeof comp !== "undefined") {
|
|
172
|
+
return comp;
|
|
173
|
+
} else {
|
|
174
|
+
// fetch from db
|
|
175
|
+
const temp = new ctor();
|
|
176
|
+
const typeId = temp.getTypeID();
|
|
177
|
+
try {
|
|
178
|
+
const rows = await db`SELECT id, data FROM components WHERE entity_id = ${this.id} AND type_id = ${typeId} AND deleted_at IS NULL`;
|
|
179
|
+
if (rows.length > 0) {
|
|
180
|
+
const row = rows[0];
|
|
181
|
+
const comp = new ctor();
|
|
182
|
+
Object.assign(comp, row.data);
|
|
183
|
+
comp.id = row.id;
|
|
184
|
+
comp.setPersisted(true);
|
|
185
|
+
comp.setDirty(false);
|
|
186
|
+
this.addComponent(comp);
|
|
187
|
+
return comp;
|
|
188
|
+
} else {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.error(`Failed to fetch component: ${error}`);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
158
198
|
@timed("Entity.save")
|
|
159
199
|
public save() {
|
|
160
200
|
return new Promise<boolean>((resolve, reject) => {
|
|
@@ -239,7 +279,7 @@ export class Entity {
|
|
|
239
279
|
public doDelete(force: boolean = false) {
|
|
240
280
|
return new Promise<boolean>(async resolve => {
|
|
241
281
|
if(!this._persisted) {
|
|
242
|
-
|
|
282
|
+
logger.warn("Entity is not persisted, cannot delete.");
|
|
243
283
|
return resolve(false);
|
|
244
284
|
}
|
|
245
285
|
try {
|
|
@@ -357,11 +397,19 @@ export class Entity {
|
|
|
357
397
|
}
|
|
358
398
|
}
|
|
359
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Find an entity by its ID. Returning populated with all components. Or null if not found.
|
|
402
|
+
* @param id Entity ID
|
|
403
|
+
* @returns Entity | null
|
|
404
|
+
*/
|
|
360
405
|
public static async FindById(id: string): Promise<Entity | null> {
|
|
406
|
+
const { default: Query } = await import("./Query");
|
|
361
407
|
const entities = await new Query().findById(id).populate().exec()
|
|
362
408
|
if(entities.length === 1) {
|
|
363
409
|
return entities[0]!;
|
|
364
410
|
}
|
|
365
411
|
return null;
|
|
366
412
|
}
|
|
367
|
-
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export default Entity;
|
package/core/EntityManager.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import ApplicationLifecycle, { ApplicationPhase } from "./ApplicationLifecycle";
|
|
2
|
-
import type {
|
|
2
|
+
import type { IEntity } from "./EntityInterface";
|
|
3
3
|
|
|
4
4
|
class EntityManager {
|
|
5
5
|
static #instance: EntityManager;
|
|
6
6
|
private dbReady = false;
|
|
7
|
-
private entityQueue:
|
|
7
|
+
private entityQueue: IEntity[] = [];
|
|
8
8
|
|
|
9
9
|
constructor() {
|
|
10
10
|
ApplicationLifecycle.addPhaseListener(async (event) => {
|
|
@@ -15,7 +15,7 @@ class EntityManager {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
public saveEntity(entity:
|
|
18
|
+
public saveEntity(entity: IEntity) {
|
|
19
19
|
return new Promise<boolean>(async resolve => {
|
|
20
20
|
if(!this.dbReady) {
|
|
21
21
|
this.entityQueue.push(entity);
|
|
@@ -27,7 +27,7 @@ class EntityManager {
|
|
|
27
27
|
})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
public deleteEntity(entity:
|
|
30
|
+
public deleteEntity(entity: IEntity, force: boolean = false) {
|
|
31
31
|
return new Promise<boolean>(async resolve => {
|
|
32
32
|
if(!this.dbReady) {
|
|
33
33
|
return resolve(false);
|