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.
- package/.github/workflows/deploy-docs.yml +57 -0
- package/LICENSE.md +1 -1
- package/README.md +2 -28
- package/TODO.md +8 -1
- package/bun.lock +3 -0
- package/config/upload.config.ts +135 -0
- package/core/App.ts +168 -4
- package/core/ArcheType.ts +122 -0
- package/core/BatchLoader.ts +100 -0
- package/core/ComponentRegistry.ts +4 -3
- package/core/Components.ts +2 -2
- package/core/Decorators.ts +15 -8
- package/core/Entity.ts +193 -14
- package/core/EntityCache.ts +15 -0
- package/core/EntityHookManager.ts +855 -0
- package/core/EntityManager.ts +12 -2
- package/core/ErrorHandler.ts +64 -7
- package/core/FileValidator.ts +284 -0
- package/core/Query.ts +503 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +89 -0
- package/core/SchedulerManager.ts +710 -0
- package/core/UploadManager.ts +261 -0
- package/core/components/UploadComponent.ts +206 -0
- package/core/decorators/EntityHooks.ts +190 -0
- package/core/decorators/ScheduledTask.ts +83 -0
- package/core/events/EntityLifecycleEvents.ts +177 -0
- package/core/processors/ImageProcessor.ts +423 -0
- package/core/storage/LocalStorageProvider.ts +290 -0
- package/core/storage/StorageProvider.ts +112 -0
- package/database/DatabaseHelper.ts +183 -58
- package/database/index.ts +5 -5
- package/database/sqlHelpers.ts +7 -0
- package/docs/README.md +149 -0
- package/docs/_coverpage.md +36 -0
- package/docs/_sidebar.md +23 -0
- package/docs/api/core.md +568 -0
- package/docs/api/hooks.md +554 -0
- package/docs/api/index.md +222 -0
- package/docs/api/query.md +678 -0
- package/docs/api/service.md +744 -0
- package/docs/core-concepts/archetypes.md +512 -0
- package/docs/core-concepts/components.md +498 -0
- package/docs/core-concepts/entity.md +314 -0
- package/docs/core-concepts/hooks.md +683 -0
- package/docs/core-concepts/query.md +588 -0
- package/docs/core-concepts/services.md +647 -0
- package/docs/examples/code-examples.md +425 -0
- package/docs/getting-started.md +337 -0
- package/docs/index.html +97 -0
- package/gql/Generator.ts +58 -35
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +65 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +19 -10
- package/rest/Generator.ts +3 -0
- package/rest/index.ts +22 -0
- package/service/Service.ts +1 -1
- package/service/ServiceRegistry.ts +10 -6
- package/service/index.ts +12 -1
- package/tests/bench/insert.bench.ts +59 -0
- package/tests/bench/relations.bench.ts +269 -0
- package/tests/bench/sorting.bench.ts +415 -0
- package/tests/component-hooks.test.ts +1409 -0
- package/tests/component.test.ts +338 -0
- package/tests/errorHandling.test.ts +155 -0
- package/tests/hooks.test.ts +666 -0
- package/tests/query-sorting.test.ts +101 -0
- package/tests/relations.test.ts +169 -0
- package/tests/scheduler.test.ts +724 -0
- package/tsconfig.json +35 -34
- package/types/graphql.types.ts +87 -0
- package/types/hooks.types.ts +141 -0
- package/types/scheduler.types.ts +165 -0
- package/types/upload.types.ts +184 -0
- package/upload/index.ts +140 -0
- package/utils/UploadHelper.ts +305 -0
- package/utils/cronParser.ts +366 -0
- package/utils/errorMessages.ts +151 -0
- 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);
|
package/core/Components.ts
CHANGED
|
@@ -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
|
}
|
package/core/Decorators.ts
CHANGED
|
@@ -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(
|
|
11
|
-
|
|
11
|
+
export function timed(hint?: string) {
|
|
12
|
+
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
13
|
+
const originalMethod = descriptor.value;
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|