bunsane 0.1.2 → 0.1.3

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.
Files changed (48) hide show
  1. package/TODO.md +1 -1
  2. package/bun.lock +156 -150
  3. package/core/App.ts +188 -31
  4. package/core/ArcheType.ts +1044 -26
  5. package/core/ComponentRegistry.ts +172 -29
  6. package/core/Components.ts +102 -24
  7. package/core/Decorators.ts +0 -1
  8. package/core/Entity.ts +55 -7
  9. package/core/EntityInterface.ts +4 -0
  10. package/core/EntityManager.ts +4 -4
  11. package/core/Query.ts +169 -3
  12. package/core/RequestLoaders.ts +101 -12
  13. package/core/SchedulerManager.ts +3 -4
  14. package/core/metadata/definitions/ArcheType.ts +9 -0
  15. package/core/metadata/definitions/Component.ts +16 -0
  16. package/core/metadata/definitions/gqlObject.ts +10 -0
  17. package/core/metadata/getMetadataStorage.ts +14 -0
  18. package/core/metadata/index.ts +17 -0
  19. package/core/metadata/metadata-storage.ts +81 -0
  20. package/database/DatabaseHelper.ts +22 -20
  21. package/database/sqlHelpers.ts +0 -2
  22. package/gql/ArchetypeOperations.ts +281 -0
  23. package/gql/Generator.ts +252 -62
  24. package/gql/helpers.ts +5 -5
  25. package/gql/index.ts +19 -17
  26. package/gql/types.ts +58 -11
  27. package/index.ts +93 -82
  28. package/package.json +39 -37
  29. package/plugins/index.ts +13 -0
  30. package/scheduler/index.ts +87 -0
  31. package/service/Service.ts +4 -0
  32. package/service/ServiceRegistry.ts +5 -1
  33. package/service/index.ts +1 -1
  34. package/swagger/decorators.ts +65 -0
  35. package/swagger/generator.ts +100 -0
  36. package/swagger/index.ts +2 -0
  37. package/tests/bench/insert.bench.ts +1 -0
  38. package/tests/bench/relations.bench.ts +1 -0
  39. package/tests/bench/sorting.bench.ts +1 -0
  40. package/tests/component-hooks-simple.test.ts +117 -0
  41. package/tests/component-hooks.test.ts +83 -31
  42. package/tests/component.test.ts +1 -0
  43. package/tests/hooks.test.ts +1 -0
  44. package/tests/query.test.ts +46 -4
  45. package/tests/relations.test.ts +1 -0
  46. package/types/app.types.ts +0 -0
  47. package/upload/index.ts +0 -2
  48. 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, new () => BaseComponent>();
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, new () => BaseComponent>();
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
- 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
+ // 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: new () => BaseComponent
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
- 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);
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: new () => BaseComponent) {
168
+ register(name: string, typeid: string, ctor: ComponentConstructor) {
94
169
  return new Promise<boolean>(async resolve => {
95
- const partitionTableName = `components_${this.sluggifyName(name)}`;
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 this.populateCurrentTables();
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 sluggifyName(name: string) {
113
- return name.toLowerCase().replace(/\s+/g, '_');
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
 
@@ -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 Reflect.metadata("compData", { isData: true, indexed: options?.indexed ?? false });
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: any) {
25
- ComponentRegistry.define(target.name, target);
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
- this._typeId = ComponentRegistry.getComponentId(this._comp_name) || generateTypeId(this._comp_name);
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
- return Object.keys(this).filter(prop => {
48
- const meta = Reflect.getMetadata("compData", Object.getPrototypeOf(this), prop);
49
- return meta && meta.isData;
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 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
- });
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
- return Object.keys(this).filter(prop => {
116
- const meta = Reflect.getMetadata("compData", Object.getPrototypeOf(this), prop);
117
- return meta && meta.isData && meta.indexed;
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
 
@@ -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: Partial<ComponentDataType<T>>): this {
43
+ public add<T extends BaseComponent>(ctor: new (...args: any[]) => T, data?: Partial<ComponentDataType<T>>): this {
43
44
  const instance = new ctor();
44
- Object.assign(instance, data);
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
- console.log("Entity is not persisted, cannot delete.");
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;
@@ -0,0 +1,4 @@
1
+ export interface IEntity {
2
+ doSave(): Promise<boolean>;
3
+ doDelete(force?: boolean): Promise<boolean>;
4
+ }
@@ -1,10 +1,10 @@
1
1
  import ApplicationLifecycle, { ApplicationPhase } from "./ApplicationLifecycle";
2
- import type { Entity } from "./Entity";
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: Entity[] = [];
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: 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: Entity, force: boolean = false) {
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);