bunsane 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.github/workflows/deploy-docs.yml +57 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -28
  4. package/TODO.md +8 -1
  5. package/bun.lock +3 -0
  6. package/config/upload.config.ts +135 -0
  7. package/core/App.ts +168 -4
  8. package/core/ArcheType.ts +122 -0
  9. package/core/BatchLoader.ts +100 -0
  10. package/core/ComponentRegistry.ts +4 -3
  11. package/core/Components.ts +2 -2
  12. package/core/Decorators.ts +15 -8
  13. package/core/Entity.ts +193 -14
  14. package/core/EntityCache.ts +15 -0
  15. package/core/EntityHookManager.ts +855 -0
  16. package/core/EntityManager.ts +12 -2
  17. package/core/ErrorHandler.ts +64 -7
  18. package/core/FileValidator.ts +284 -0
  19. package/core/Query.ts +503 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +89 -0
  22. package/core/SchedulerManager.ts +710 -0
  23. package/core/UploadManager.ts +261 -0
  24. package/core/components/UploadComponent.ts +206 -0
  25. package/core/decorators/EntityHooks.ts +190 -0
  26. package/core/decorators/ScheduledTask.ts +83 -0
  27. package/core/events/EntityLifecycleEvents.ts +177 -0
  28. package/core/processors/ImageProcessor.ts +423 -0
  29. package/core/storage/LocalStorageProvider.ts +290 -0
  30. package/core/storage/StorageProvider.ts +112 -0
  31. package/database/DatabaseHelper.ts +183 -58
  32. package/database/index.ts +5 -5
  33. package/database/sqlHelpers.ts +7 -0
  34. package/docs/README.md +149 -0
  35. package/docs/_coverpage.md +36 -0
  36. package/docs/_sidebar.md +23 -0
  37. package/docs/api/core.md +568 -0
  38. package/docs/api/hooks.md +554 -0
  39. package/docs/api/index.md +222 -0
  40. package/docs/api/query.md +678 -0
  41. package/docs/api/service.md +744 -0
  42. package/docs/core-concepts/archetypes.md +512 -0
  43. package/docs/core-concepts/components.md +498 -0
  44. package/docs/core-concepts/entity.md +314 -0
  45. package/docs/core-concepts/hooks.md +683 -0
  46. package/docs/core-concepts/query.md +588 -0
  47. package/docs/core-concepts/services.md +647 -0
  48. package/docs/examples/code-examples.md +425 -0
  49. package/docs/getting-started.md +337 -0
  50. package/docs/index.html +97 -0
  51. package/gql/Generator.ts +58 -35
  52. package/gql/decorators/Upload.ts +176 -0
  53. package/gql/helpers.ts +67 -0
  54. package/gql/index.ts +65 -31
  55. package/gql/types.ts +1 -1
  56. package/index.ts +79 -11
  57. package/package.json +19 -10
  58. package/rest/Generator.ts +3 -0
  59. package/rest/index.ts +22 -0
  60. package/service/Service.ts +1 -1
  61. package/service/ServiceRegistry.ts +10 -6
  62. package/service/index.ts +12 -1
  63. package/tests/bench/insert.bench.ts +59 -0
  64. package/tests/bench/relations.bench.ts +269 -0
  65. package/tests/bench/sorting.bench.ts +415 -0
  66. package/tests/component-hooks.test.ts +1409 -0
  67. package/tests/component.test.ts +338 -0
  68. package/tests/errorHandling.test.ts +155 -0
  69. package/tests/hooks.test.ts +666 -0
  70. package/tests/query-sorting.test.ts +101 -0
  71. package/tests/relations.test.ts +169 -0
  72. package/tests/scheduler.test.ts +724 -0
  73. package/tsconfig.json +35 -34
  74. package/types/graphql.types.ts +87 -0
  75. package/types/hooks.types.ts +141 -0
  76. package/types/scheduler.types.ts +165 -0
  77. package/types/upload.types.ts +184 -0
  78. package/upload/index.ts +140 -0
  79. package/utils/UploadHelper.ts +305 -0
  80. package/utils/cronParser.ts +366 -0
  81. package/utils/errorMessages.ts +151 -0
  82. package/core/Events.ts +0 -0
@@ -0,0 +1,100 @@
1
+ import { Entity } from "core/Entity";
2
+ import { BaseComponent } from "core/Components";
3
+ import { timed } from "./Decorators";
4
+ import db from "../database";
5
+ import { sql } from "bun";
6
+
7
+ // Phase 2A: Memory Pooling for Entity Objects
8
+ class EntityPool {
9
+ private static instance: EntityPool;
10
+ private pool: Map<string, Entity[]> = new Map();
11
+ private maxPoolSize = 1000;
12
+
13
+ static getInstance(): EntityPool {
14
+ if (!EntityPool.instance) {
15
+ EntityPool.instance = new EntityPool();
16
+ }
17
+ return EntityPool.instance;
18
+ }
19
+
20
+ get(entityId: string): Entity | null {
21
+ const entities = this.pool.get(entityId);
22
+ if (entities && entities.length > 0) {
23
+ return entities.pop()!;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ put(entity: Entity): void {
29
+ const entityId = entity.id;
30
+ let entities = this.pool.get(entityId);
31
+ if (!entities) {
32
+ entities = [];
33
+ this.pool.set(entityId, entities);
34
+ }
35
+ if (entities.length < this.maxPoolSize) {
36
+ entities.push(entity);
37
+ }
38
+ }
39
+
40
+ clear(): void {
41
+ this.pool.clear();
42
+ }
43
+ }
44
+
45
+ export class BatchLoader {
46
+ private static entityPool = EntityPool.getInstance();
47
+
48
+ @timed("BatchLoader.loadRelatedEntities")
49
+ static async loadRelatedEntities<C extends BaseComponent & { value: string }>(
50
+ entities: Entity[],
51
+ component: new () => C,
52
+ loader: (ids: string[]) => Promise<Entity[]>
53
+ ): Promise<Map<string, Entity>> {
54
+ const ids: string[] = [];
55
+ for (const entity of entities) {
56
+ const data = await entity.get(component) as any;
57
+ if (data?.value) {
58
+ ids.push(data.value);
59
+ }
60
+ }
61
+ const uniqueIds = [...new Set(ids)];
62
+ const relatedEntities = await loader(uniqueIds);
63
+ const map = new Map<string, Entity>();
64
+ for (const related of relatedEntities) {
65
+ map.set(related.id, related);
66
+ }
67
+ return map;
68
+ }
69
+
70
+ @timed("BatchLoader.loadRelatedEntitiesBatched")
71
+ static async loadRelatedEntitiesBatched<C extends BaseComponent>(
72
+ entities: Entity[],
73
+ component: new () => C,
74
+ loader: (ids: string[]) => Promise<Entity[]>
75
+ ): Promise<Map<string, Entity>> {
76
+ if (entities.length === 0) return new Map();
77
+
78
+ const comp = new component();
79
+ const typeId = comp.getTypeID();
80
+ const parentIds = entities.map(e => e.id);
81
+
82
+ const rows = await db`
83
+ SELECT c.entity_id, (c.data->>'value') AS related_id
84
+ FROM components c
85
+ WHERE c.entity_id IN ${sql(parentIds)}
86
+ AND c.type_id = ${typeId}
87
+ AND c.deleted_at IS NULL
88
+ `;
89
+
90
+ const uniqueIds = [...new Set(rows.map((r: any) => r.related_id).filter(Boolean))] as string[];
91
+ if (uniqueIds.length === 0) return new Map();
92
+
93
+ const relatedEntities = await loader(uniqueIds);
94
+ const map = new Map<string, Entity>();
95
+ for (const related of relatedEntities) {
96
+ map.set(related.id, related);
97
+ }
98
+ return map;
99
+ }
100
+ }
@@ -1,6 +1,6 @@
1
1
  import { generateTypeId, type BaseComponent } from "./Components";
2
2
  import ApplicationLifecycle, { ApplicationPhase } from "./ApplicationLifecycle";
3
- import { CreateComponentPartitionTable } from "database/DatabaseHelper";
3
+ import { CreateComponentPartitionTable, UpdateComponentIndexes } from "database/DatabaseHelper";
4
4
  import { GetSchema } from "database/DatabaseHelper";
5
5
  import { logger as MainLogger } from "./Logger";
6
6
  const logger = MainLogger.child({ scope: "ComponentRegistry" });
@@ -94,13 +94,14 @@ class ComponentRegistry {
94
94
  return new Promise<boolean>(async resolve => {
95
95
  const partitionTableName = `components_${this.sluggifyName(name)}`;
96
96
  await this.populateCurrentTables();
97
+ const instance = new ctor();
98
+ const indexedProps = instance.indexedProperties();
97
99
  if (!this.currentTables.includes(partitionTableName)) {
98
100
  logger.trace(`Partition table ${partitionTableName} does not exist. Creating... name: ${name}, typeId: ${typeid}`);
99
- const instance = new ctor();
100
- const indexedProps = instance.indexedProperties();
101
101
  await CreateComponentPartitionTable(name, typeid, indexedProps);
102
102
  await this.populateCurrentTables();
103
103
  }
104
+ await UpdateComponentIndexes(partitionTableName, indexedProps);
104
105
  this.componentsMap.set(name, typeid);
105
106
  this.typeIdToCtor.set(typeid, ctor);
106
107
  resolve(true);
@@ -45,7 +45,7 @@ export class BaseComponent {
45
45
 
46
46
  properties(): string[] {
47
47
  return Object.keys(this).filter(prop => {
48
- const meta = Reflect.getMetadata("compData", this, prop);
48
+ const meta = Reflect.getMetadata("compData", Object.getPrototypeOf(this), prop);
49
49
  return meta && meta.isData;
50
50
  });
51
51
  }
@@ -113,7 +113,7 @@ export class BaseComponent {
113
113
 
114
114
  indexedProperties(): string[] {
115
115
  return Object.keys(this).filter(prop => {
116
- const meta = Reflect.getMetadata("compData", this, prop);
116
+ const meta = Reflect.getMetadata("compData", Object.getPrototypeOf(this), prop);
117
117
  return meta && meta.isData && meta.indexed;
118
118
  });
119
119
  }
@@ -1,3 +1,4 @@
1
+ import { logger } from "./Logger";
1
2
  export function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
2
3
  const originalMethod = descriptor.value;
3
4
 
@@ -7,14 +8,20 @@ export function log(target: any, propertyKey: string, descriptor: PropertyDescri
7
8
  };
8
9
  }
9
10
 
10
- export function timed(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
11
- const originalMethod = descriptor.value;
11
+ export function timed(hint?: string) {
12
+ return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
13
+ const originalMethod = descriptor.value;
12
14
 
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;
15
+ descriptor.value = async function(...args: any[]) {
16
+ if(process.env.NODE_ENV !== 'production') {
17
+ const start = performance.now();
18
+ const result = await originalMethod.apply(this, args);
19
+ const end = performance.now();
20
+ logger.trace(`Execution time for ${propertyKey}${hint ? ` (${hint})` : ''}: ${end - start} ms`);
21
+ return result;
22
+ } else {
23
+ return await originalMethod.apply(this, args);
24
+ }
25
+ };
19
26
  };
20
27
  }
package/core/Entity.ts CHANGED
@@ -6,12 +6,15 @@ import ComponentRegistry from "./ComponentRegistry";
6
6
  import { uuidv7 } from "utils/uuid";
7
7
  import { sql } from "bun";
8
8
  import Query from "./Query";
9
-
9
+ import { timed } from "./Decorators";
10
+ import EntityHookManager from "./EntityHookManager";
11
+ import { EntityCreatedEvent, EntityUpdatedEvent, EntityDeletedEvent, ComponentAddedEvent, ComponentUpdatedEvent, ComponentRemovedEvent } from "./events/EntityLifecycleEvents";
10
12
 
11
13
  export class Entity {
12
14
  id: string;
13
15
  public _persisted: boolean = false;
14
16
  private components: Map<string, BaseComponent> = new Map<string, BaseComponent>();
17
+ private removedComponents: Set<string> = new Set<string>();
15
18
  protected _dirty: boolean = false;
16
19
 
17
20
  constructor(id?: string) {
@@ -23,7 +26,7 @@ export class Entity {
23
26
  return new Entity();
24
27
  }
25
28
 
26
- private addComponent(component: BaseComponent): Entity {
29
+ protected addComponent(component: BaseComponent): Entity {
27
30
  this.components.set(component.getTypeID(), component);
28
31
  return this;
29
32
  }
@@ -36,10 +39,19 @@ export class Entity {
36
39
  * Adds a new component to the entity.
37
40
  * Use like: entity.add(Component, { value: "Test" })
38
41
  */
39
- public add<T extends BaseComponent>(ctor: new (...args: any[]) => T, data: ComponentDataType<T>): this {
42
+ public add<T extends BaseComponent>(ctor: new (...args: any[]) => T, data: Partial<ComponentDataType<T>>): this {
40
43
  const instance = new ctor();
41
44
  Object.assign(instance, data);
42
45
  this.addComponent(instance);
46
+
47
+ // Fire component added event
48
+ try {
49
+ EntityHookManager.executeHooks(new ComponentAddedEvent(this, instance));
50
+ } catch (error) {
51
+ logger.error(`Error firing component added hook for ${instance.getTypeID()}: ${error}`);
52
+ // Don't fail the add operation if hooks fail
53
+ }
54
+
43
55
  return this;
44
56
  }
45
57
 
@@ -49,24 +61,65 @@ export class Entity {
49
61
  * If it doesn't exist, it adds a new component.
50
62
  * Use like: entity.set(Component, { value: "Test" })
51
63
  */
52
- public async set<T extends BaseComponent>(ctor: new (...args: any[]) => T, data: Record<string, any>): Promise<this> {
64
+ public async set<T extends BaseComponent>(ctor: new (...args: any[]) => T, data: Partial<ComponentDataType<T>>): Promise<this> {
53
65
  await this.get(ctor);
54
66
 
55
67
  const component = Array.from(this.components.values()).find(comp => comp instanceof ctor) as T;
56
68
  if (component) {
57
- console.log("Updating Existing Component", component.getTypeID())
69
+ // Store old data for the update event
70
+ const oldData = { ...component };
71
+
58
72
  // Update existing component
59
73
  Object.assign(component, data);
60
74
  component.setDirty(true);
61
75
  this._dirty = true;
76
+
77
+ // Fire component updated event
78
+ try {
79
+ EntityHookManager.executeHooks(new ComponentUpdatedEvent(this, component, oldData, component));
80
+ } catch (error) {
81
+ logger.error(`Error firing component updated hook for ${component.getTypeID()}: ${error}`);
82
+ // Don't fail the set operation if hooks fail
83
+ }
62
84
  } else {
63
85
  // Add new component
64
- console.log("Adding New Component")
65
- this.add(ctor, data as any);
66
- this._dirty = true;
86
+ this.add(ctor, data);
87
+ // Note: add() already fires ComponentAddedEvent, so we don't need to fire it again
67
88
  }
68
89
  return this;
69
90
  }
91
+
92
+ /**
93
+ * Removes a component from the entity.
94
+ * Use like: entity.remove(Component)
95
+ * WARNING: This will delete the component from the database upon saving the entity.
96
+ * If you want to keep the component in the database but just remove it from the entity instance,
97
+ * consider implementing a different method.
98
+ */
99
+ public remove<T extends BaseComponent>(ctor: new (...args: any[]) => T): boolean {
100
+ const component = Array.from(this.components.values()).find(comp => comp instanceof ctor) as T;
101
+
102
+ if (component) {
103
+ // Track the component type for database deletion
104
+ this.removedComponents.add(component.getTypeID());
105
+
106
+ // Remove the component from the map
107
+ this.components.delete(component.getTypeID());
108
+ this._dirty = true;
109
+
110
+ // Fire component removed event
111
+ try {
112
+ EntityHookManager.executeHooks(new ComponentRemovedEvent(this, component));
113
+ } catch (error) {
114
+ logger.error(`Error firing component removed hook for ${component.getTypeID()}: ${error}`);
115
+ // Don't fail the remove operation if hooks fail
116
+ }
117
+
118
+ return true;
119
+ }
120
+
121
+ return false;
122
+ }
70
123
  /**
71
124
  * Get component from entities. If entity is populated in query the component will get within the entitiy
72
125
  * If not it will fetch from database
@@ -82,7 +135,7 @@ export class Entity {
82
135
  const temp = new ctor();
83
136
  const typeId = temp.getTypeID();
84
137
  try {
85
- const rows = await db`SELECT id, data FROM components WHERE entity_id = ${this.id} AND type_id = ${typeId}`;
138
+ const rows = await db`SELECT id, data FROM components WHERE entity_id = ${this.id} AND type_id = ${typeId} AND deleted_at IS NULL`;
86
139
  if (rows.length > 0) {
87
140
  const row = rows[0];
88
141
  const comp = new ctor();
@@ -102,39 +155,122 @@ export class Entity {
102
155
  }
103
156
  }
104
157
 
158
+ @timed("Entity.save")
105
159
  public save() {
106
- return EntityManager.saveEntity(this);
160
+ return new Promise<boolean>((resolve, reject) => {
161
+ // Add timeout to prevent hanging
162
+ const timeout = setTimeout(() => {
163
+ logger.error(`Entity save timeout for entity ${this.id}`);
164
+ reject(new Error(`Entity save timeout for entity ${this.id}`));
165
+ }, 30000); // 30 second timeout
166
+
167
+ this.doSave()
168
+ .then(result => {
169
+ clearTimeout(timeout);
170
+ resolve(result);
171
+ })
172
+ .catch(error => {
173
+ clearTimeout(timeout);
174
+ reject(error);
175
+ });
176
+ });
107
177
  }
108
178
 
179
+
180
+
109
181
  public doSave() {
110
182
  return new Promise<boolean>(async resolve => {
111
183
  if(!this._dirty) {
112
- console.log("Entity is not dirty, no need to save.");
184
+ logger.trace("Entity is not dirty, no need to save.");
113
185
  return resolve(true);
114
186
  }
187
+
188
+ const wasNew = !this._persisted;
189
+ const changedComponents = this.getDirtyComponents();
190
+
115
191
  await db.transaction(async (trx) => {
116
192
  if(!this._persisted) {
117
193
  await trx`INSERT INTO entities (id) VALUES (${this.id}) ON CONFLICT DO NOTHING`;
118
194
  this._persisted = true;
119
195
  }
196
+
197
+ // Delete removed components from database
198
+ if (this.removedComponents.size > 0) {
199
+ const typeIds = Array.from(this.removedComponents);
200
+ await trx`DELETE FROM components WHERE entity_id = ${this.id} AND type_id IN ${sql(typeIds)}`;
201
+ await trx`DELETE FROM entity_components WHERE entity_id = ${this.id} AND type_id IN ${sql(typeIds)}`;
202
+ this.removedComponents.clear();
203
+ }
204
+
120
205
  if(this.components.size === 0) {
121
206
  logger.trace(`No components to save for entity ${this.id}`);
122
207
  return;
123
208
  }
124
209
  const waitable = [];
125
-
126
210
  for(const comp of this.components.values()) {
127
211
  waitable.push(comp.save(trx, this.id));
128
212
  }
129
-
130
213
  await Promise.all(waitable);
131
214
  });
215
+
132
216
  this._dirty = false;
217
+
218
+ // Fire lifecycle events after successful save
219
+ try {
220
+ if (wasNew) {
221
+ await EntityHookManager.executeHooks(new EntityCreatedEvent(this));
222
+ } else if (changedComponents.length > 0) {
223
+ await EntityHookManager.executeHooks(new EntityUpdatedEvent(this, changedComponents));
224
+ }
225
+ } catch (error) {
226
+ logger.error(`Error firing lifecycle hooks for entity ${this.id}: ${error}`);
227
+ // Don't fail the save operation if hooks fail
228
+ }
229
+
133
230
  resolve(true);
134
231
  })
135
232
 
136
233
  }
137
234
 
235
+ public delete(force: boolean = false) {
236
+ return EntityManager.deleteEntity(this, force);
237
+ }
238
+
239
+ public doDelete(force: boolean = false) {
240
+ return new Promise<boolean>(async resolve => {
241
+ if(!this._persisted) {
242
+ console.log("Entity is not persisted, cannot delete.");
243
+ return resolve(false);
244
+ }
245
+ try {
246
+ await db.transaction(async (trx) => {
247
+ if(force) {
248
+ await trx`DELETE FROM entity_components WHERE entity_id = ${this.id}`;
249
+ await trx`DELETE FROM components WHERE entity_id = ${this.id}`;
250
+ await trx`DELETE FROM entities WHERE id = ${this.id}`;
251
+ } else {
252
+ await trx`UPDATE entities SET deleted_at = CURRENT_TIMESTAMP WHERE id = ${this.id} AND deleted_at IS NULL`;
253
+ await trx`UPDATE entity_components SET deleted_at = CURRENT_TIMESTAMP WHERE entity_id = ${this.id} AND deleted_at IS NULL`;
254
+ await trx`UPDATE components SET deleted_at = CURRENT_TIMESTAMP WHERE entity_id = ${this.id} AND deleted_at IS NULL`;
255
+ }
256
+ });
257
+
258
+ // Fire lifecycle event after successful deletion
259
+ try {
260
+ await EntityHookManager.executeHooks(new EntityDeletedEvent(this, !force));
261
+ } catch (error) {
262
+ logger.error(`Error firing delete lifecycle hook for entity ${this.id}: ${error}`);
263
+ // Don't fail the delete operation if hooks fail
264
+ }
265
+
266
+ resolve(true);
267
+ } catch (error) {
268
+ logger.error(`Failed to delete entity: ${error}`);
269
+ resolve(false);
270
+ }
271
+ })
272
+ }
273
+
138
274
  public setPersisted(persisted: boolean) {
139
275
  this._persisted = persisted;
140
276
  }
@@ -143,13 +279,28 @@ export class Entity {
143
279
  this._dirty = dirty;
144
280
  }
145
281
 
282
+ /**
283
+ * Get list of component type IDs that are dirty
284
+ */
285
+ private getDirtyComponents(): string[] {
286
+ const dirtyComponents: string[] = [];
287
+ for (const component of this.components.values()) {
288
+ if ((component as any)._dirty) {
289
+ dirtyComponents.push(component.getTypeID());
290
+ }
291
+ }
292
+ return dirtyComponents;
293
+ }
294
+
295
+
296
+ @timed("Entity.LoadMultiple")
146
297
  public static async LoadMultiple(ids: string[]): Promise<Entity[]> {
147
298
  if (ids.length === 0) return [];
148
299
 
149
300
  const components = await db`
150
301
  SELECT c.id, c.entity_id, c.type_id, c.data
151
302
  FROM components c
152
- WHERE c.entity_id IN ${sql(ids)}
303
+ WHERE c.entity_id IN ${sql(ids)} AND c.deleted_at IS NULL
153
304
  `;
154
305
 
155
306
  const entitiesMap = new Map<string, Entity>();
@@ -178,6 +329,34 @@ export class Entity {
178
329
  return Array.from(entitiesMap.values());
179
330
  }
180
331
 
332
+ public static async LoadComponents(entities: Entity[], componentIds: string[]): Promise<void> {
333
+ if (entities.length === 0 || componentIds.length === 0) return;
334
+
335
+ const entityIds = entities.map(e => e.id);
336
+
337
+ const components = await db`
338
+ SELECT c.id, c.entity_id, c.type_id, c.data
339
+ FROM components c
340
+ WHERE c.entity_id IN ${sql(entityIds)} AND c.type_id IN ${sql(componentIds)} AND c.deleted_at IS NULL
341
+ `;
342
+
343
+ for (const row of components) {
344
+ const { id, entity_id, type_id, data } = row;
345
+ const entity = entities.find(e => e.id === entity_id);
346
+ if (entity) {
347
+ const ctor = ComponentRegistry.getConstructor(type_id);
348
+ if (ctor) {
349
+ const comp = new ctor();
350
+ Object.assign(comp, data);
351
+ comp.id = id;
352
+ comp.setPersisted(true);
353
+ comp.setDirty(false);
354
+ entity.addComponent(comp);
355
+ }
356
+ }
357
+ }
358
+ }
359
+
181
360
  public static async FindById(id: string): Promise<Entity | null> {
182
361
  const entities = await new Query().findById(id).populate().exec()
183
362
  if(entities.length === 1) {
@@ -0,0 +1,15 @@
1
+ import type { RequestLoaders, ComponentData } from './RequestLoaders';
2
+ import { Entity } from './Entity';
3
+
4
+ export async function getEntityById(ctx: { locals: { loaders: RequestLoaders } }, id: string): Promise<Entity | null> {
5
+ return ctx.locals.loaders.entityById.load(id);
6
+ }
7
+
8
+ export async function getComponent(ctx: { locals: { loaders: RequestLoaders } }, entityId: string, typeId: number): Promise<ComponentData | null> {
9
+ return ctx.locals.loaders.componentsByEntityType.load({ entityId, typeId });
10
+ }
11
+
12
+ export async function preloadComponents(ctx: { locals: { loaders: RequestLoaders } }, entityIds: string[], typeId: number): Promise<void> {
13
+ const keys = entityIds.map(entityId => ({ entityId, typeId }));
14
+ await ctx.locals.loaders.componentsByEntityType.loadMany(keys);
15
+ }