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
package/core/Query.ts CHANGED
@@ -7,7 +7,7 @@ import db from "database";
7
7
  import { timed } from "./Decorators";
8
8
  import { inList } from "../database/sqlHelpers";
9
9
 
10
- export type FilterOperator = "=" | ">" | "<" | ">=" | "<=" | "!=" | "LIKE" | "IN" | "NOT IN";
10
+ export type FilterOperator = "=" | ">" | "<" | ">=" | "<=" | "!=" | "LIKE" | "IN" | "NOT IN" | string;
11
11
 
12
12
  export const FilterOp = {
13
13
  EQ: "=" as FilterOperator,
@@ -50,6 +50,12 @@ class Query {
50
50
  private eagerComponents: Set<string> = new Set<string>();
51
51
  private sortOrders: SortOrder[] = [];
52
52
 
53
+ private static customFilterBuilders: Map<string, (filter: QueryFilter, alias: string, paramIndex: number) => { sql: string, params: any[], newParamIndex: number }> = new Map();
54
+
55
+ public static registerFilterBuilder(operator: string, builder: (filter: QueryFilter, alias: string, paramIndex: number) => { sql: string, params: any[], newParamIndex: number }) {
56
+ this.customFilterBuilders.set(operator, builder);
57
+ }
58
+
53
59
  static filterOp = FilterOp;
54
60
 
55
61
  public findById(id: string) {
@@ -118,6 +124,11 @@ class Query {
118
124
  private buildFilterCondition(filter: QueryFilter, alias: string, paramIndex: number): { sql: string, params: any[], newParamIndex: number } {
119
125
  const { field, operator, value } = filter;
120
126
 
127
+ // Check for custom filter builders first
128
+ if (Query.customFilterBuilders.has(operator)) {
129
+ return Query.customFilterBuilders.get(operator)!(filter, alias, paramIndex);
130
+ }
131
+
121
132
  // Build JSON path for nested properties (e.g., "parent.child" -> data->'parent'->>'child')
122
133
  const jsonPath = this.buildJsonPath(field, alias);
123
134
 
@@ -258,6 +269,10 @@ class Query {
258
269
  return this;
259
270
  }
260
271
 
272
+ public count(): Promise<number> {
273
+ return this.doCount();
274
+ }
275
+
261
276
  @timed("Query.exec")
262
277
  public async exec(): Promise<Entity[]> {
263
278
  return new Promise<Entity[]>((resolve, reject) => {
@@ -339,7 +354,6 @@ class Query {
339
354
  let queryStr: any;
340
355
  let requiredOnlyQueryResult: any;
341
356
  if (componentCount === 1) {
342
- // Phase 2A: Optimize single component sorting with JOIN
343
357
  if (this.sortOrders.length > 0) {
344
358
  const typeId = componentIds[0]!;
345
359
  const sortExpression = this.buildSortExpressionForSingleComponent(typeId, "c");
@@ -679,7 +693,9 @@ class Query {
679
693
 
680
694
  const filteredResult = await db.unsafe(sql, params);
681
695
  return filteredResult.map((row: any) => row.id);
682
- } private async getIdsWithFiltersAndExclusions(componentIds: string[], excludedIds: string[], componentCount: number, limit?: number | null, offset?: number): Promise<string[]> {
696
+ }
697
+
698
+ private async getIdsWithFiltersAndExclusions(componentIds: string[], excludedIds: string[], componentCount: number, limit?: number | null, offset?: number): Promise<string[]> {
683
699
  const entityIds = await this.getIdsWithFilters(componentIds, componentCount);
684
700
 
685
701
  if (entityIds.length === 0) {
@@ -709,6 +725,156 @@ class Query {
709
725
  const exclusionResult = await query;
710
726
  return exclusionResult.map((row: any) => row.id);
711
727
  }
728
+
729
+ private async doCount(): Promise<number> {
730
+ const componentIds = Array.from(this.requiredComponents);
731
+ const excludedIds = Array.from(this.excludedComponents);
732
+ const componentCount = componentIds.length;
733
+ const hasRequired = componentCount > 0;
734
+ const hasExcluded = excludedIds.length > 0;
735
+ const hasFilters = this.componentFilters.size > 0;
736
+ const hasWithId = this.withId !== null;
737
+
738
+ switch (true) {
739
+ case !hasRequired && !hasExcluded && !hasWithId:
740
+ return 0;
741
+ case !hasRequired && !hasExcluded && hasWithId:
742
+ const result = await db`SELECT COUNT(*) as count FROM entities WHERE id = ${this.withId} AND deleted_at IS NULL`;
743
+ return parseInt(result[0].count);
744
+ case hasRequired && hasExcluded && hasFilters:
745
+ return await this.getCountWithFiltersAndExclusions(componentIds, excludedIds, componentCount);
746
+ case hasRequired && hasExcluded:
747
+ const componentIdsString = inList(componentIds, 1);
748
+ const excludedIdsString = inList(excludedIds, componentIdsString.newParamIndex);
749
+ const excludedQuery = db`
750
+ SELECT COUNT(*) as count FROM (
751
+ SELECT ec.entity_id
752
+ FROM entity_components ec
753
+ WHERE ec.type_id IN ${db.unsafe(componentIdsString.sql, componentIdsString.params)} AND ec.deleted_at IS NULL
754
+ ${this.withId ? db`AND ec.entity_id = ${this.withId}` : db``}
755
+ AND NOT EXISTS (
756
+ SELECT 1 FROM entity_components ec_ex
757
+ WHERE ec_ex.entity_id = ec.entity_id AND ec_ex.type_id IN ${db.unsafe(excludedIdsString.sql, excludedIdsString.params)} AND ec_ex.deleted_at IS NULL
758
+ )
759
+ GROUP BY ec.entity_id
760
+ HAVING COUNT(DISTINCT ec.type_id) = ${componentCount}
761
+ ) as subquery
762
+ `;
763
+ const excludedResult = await excludedQuery;
764
+ return parseInt(excludedResult[0].count);
765
+ case hasRequired && hasFilters:
766
+ return await this.getCountWithFilters(componentIds, componentCount);
767
+ case hasRequired:
768
+ if (componentCount === 1) {
769
+ const countQuery = db`SELECT COUNT(*) as count FROM entity_components WHERE type_id = ${componentIds[0]} ${this.withId ? db`AND entity_id = ${this.withId}` : db``} AND deleted_at IS NULL`;
770
+ const countResult = await countQuery;
771
+ return parseInt(countResult[0].count);
772
+ } else {
773
+ const compIds = inList(componentIds, 1);
774
+ const multiComponentQuery = db`
775
+ SELECT COUNT(*) as count FROM (
776
+ SELECT entity_id FROM entity_components
777
+ WHERE type_id IN ${db.unsafe(compIds.sql, compIds.params)} ${this.withId ? db`AND entity_id = ${this.withId}` : db``} AND deleted_at IS NULL
778
+ GROUP BY entity_id
779
+ HAVING COUNT(DISTINCT type_id) = ${componentCount}
780
+ ) as subquery
781
+ `;
782
+ const multiComponentResult = await multiComponentQuery;
783
+ return parseInt(multiComponentResult[0].count);
784
+ }
785
+ case hasExcluded:
786
+ const onlyExcludedIdsString = inList(excludedIds, 1);
787
+ const onlyExcludedQuery = db`
788
+ SELECT COUNT(*) as count FROM (
789
+ SELECT DISTINCT ec.entity_id
790
+ FROM entity_components ec
791
+ WHERE ${this.withId ? db`ec.entity_id = ${this.withId} AND ` : db``} NOT EXISTS (
792
+ SELECT 1 FROM entity_components ec_ex
793
+ WHERE ec_ex.entity_id = ec.entity_id AND ec_ex.type_id IN ${db.unsafe(onlyExcludedIdsString.sql, onlyExcludedIdsString.params)} AND ec_ex.deleted_at IS NULL
794
+ )
795
+ AND ec.deleted_at IS NULL
796
+ ) as subquery
797
+ `;
798
+ const onlyExcludedResult = await onlyExcludedQuery;
799
+ return parseInt(onlyExcludedResult[0].count);
800
+ default:
801
+ return 0;
802
+ }
803
+ }
804
+
805
+ private async getCountWithFilters(componentIds: string[], componentCount: number): Promise<number> {
806
+ let params: any[] = [];
807
+ let paramIndex = 1;
808
+ const compIds = inList(componentIds, paramIndex);
809
+ params.push(...compIds.params);
810
+ paramIndex = compIds.newParamIndex;
811
+
812
+ const joins: string[] = [];
813
+ let joinIndex = 0;
814
+ for (const [typeId, filters] of this.componentFilters.entries()) {
815
+ if (componentIds.includes(typeId)) {
816
+ const alias = `c${joinIndex}`;
817
+ joins.push(`JOIN components ${alias} ON ec.entity_id = ${alias}.entity_id AND ${alias}.type_id = $${paramIndex} AND ${alias}.deleted_at IS NULL`);
818
+ params.push(typeId);
819
+ paramIndex++;
820
+ joinIndex++;
821
+ }
822
+ }
823
+
824
+ let sql: string = `SELECT COUNT(*) as count FROM (SELECT DISTINCT ec.entity_id FROM entity_components ec ${joins.join(' ')} WHERE ec.type_id IN ${compIds.sql} AND ec.deleted_at IS NULL`;
825
+
826
+ if (this.withId) {
827
+ sql += ` AND ec.entity_id = $${paramIndex}`;
828
+ params.push(this.withId);
829
+ paramIndex++;
830
+ }
831
+
832
+ joinIndex = 0;
833
+ for (const [typeId, filters] of this.componentFilters.entries()) {
834
+ if (componentIds.includes(typeId)) {
835
+ const alias = `c${joinIndex}`;
836
+ const filterConditions = this.buildFilterWhereClause(typeId, filters, alias, paramIndex);
837
+ if (filterConditions.sql) {
838
+ sql += ` AND ${filterConditions.sql}`;
839
+ params.push(...filterConditions.params);
840
+ paramIndex = filterConditions.newParamIndex;
841
+ }
842
+ joinIndex++;
843
+ }
844
+ }
845
+
846
+ sql += ` GROUP BY ec.entity_id HAVING COUNT(DISTINCT ec.type_id) = $${paramIndex}) as subquery`;
847
+ params.push(componentCount);
848
+
849
+ const filteredResult = await db.unsafe(sql, params);
850
+ return parseInt(filteredResult[0].count);
851
+ }
852
+
853
+ private async getCountWithFiltersAndExclusions(componentIds: string[], excludedIds: string[], componentCount: number): Promise<number> {
854
+ const entityIds = await this.getIdsWithFilters(componentIds, componentCount);
855
+
856
+ if (entityIds.length === 0) {
857
+ return 0;
858
+ }
859
+
860
+ const idsList = sql(entityIds);
861
+ const excludedList = inList(excludedIds, 1);
862
+ const query = db`
863
+ SELECT COUNT(*) as count FROM (
864
+ WITH entity_list AS (
865
+ SELECT unnest(${idsList}) as id
866
+ )
867
+ SELECT el.id
868
+ FROM entity_list el
869
+ WHERE NOT EXISTS (
870
+ SELECT 1 FROM entity_components ec
871
+ WHERE ec.entity_id = el.id AND ec.type_id IN ${db.unsafe(excludedList.sql, excludedList.params)} AND ec.deleted_at IS NULL
872
+ )
873
+ ) as subquery
874
+ `;
875
+ const exclusionResult = await query;
876
+ return parseInt(exclusionResult[0].count);
877
+ }
712
878
  }
713
879
 
714
880
  export default Query;
@@ -2,9 +2,12 @@ import DataLoader from 'dataloader';
2
2
  import { Entity } from './Entity';
3
3
  import db from '../database';
4
4
  import { inList } from '../database/sqlHelpers';
5
+ import {logger as MainLogger} from './Logger';
6
+ const logger = MainLogger.child({ module: 'RequestLoaders' });
7
+ import { getMetadataStorage } from './metadata';
5
8
 
6
9
  export type ComponentData = {
7
- typeId: number;
10
+ typeId: string;
8
11
  data: any;
9
12
  createdAt: Date;
10
13
  updatedAt: Date;
@@ -13,7 +16,8 @@ export type ComponentData = {
13
16
 
14
17
  export type RequestLoaders = {
15
18
  entityById: DataLoader<string, Entity | null>;
16
- componentsByEntityType: DataLoader<{ entityId: string; typeId: number }, ComponentData | null>;
19
+ componentsByEntityType: DataLoader<{ entityId: string; typeId: string }, ComponentData | null>;
20
+ relationsByEntityField: DataLoader<{ entityId: string; relationField: string; relatedType: string; foreignKey?: string }, Entity[]>;
17
21
  };
18
22
 
19
23
  export function createRequestLoaders(db: any): RequestLoaders {
@@ -21,12 +25,13 @@ export function createRequestLoaders(db: any): RequestLoaders {
21
25
  const startTime = Date.now();
22
26
  try {
23
27
  const uniqueIds = [...new Set(ids)];
24
- const rows = await db`
28
+ const idList = inList(uniqueIds, 1);
29
+ const rows = await db.unsafe(`
25
30
  SELECT id
26
31
  FROM entities
27
- WHERE id IN ${inList(uniqueIds, 1)}
32
+ WHERE id IN ${idList.sql}
28
33
  AND deleted_at IS NULL
29
- `;
34
+ `, idList.params);
30
35
  const entities = rows.map((row: any) => {
31
36
  const entity = new Entity(row.id);
32
37
  entity.setPersisted(true);
@@ -47,19 +52,21 @@ export function createRequestLoaders(db: any): RequestLoaders {
47
52
  }
48
53
  });
49
54
 
50
- const componentsByEntityType = new DataLoader<{ entityId: string; typeId: number }, ComponentData | null>(
51
- async (keys: readonly { entityId: string; typeId: number }[]) => {
55
+ const componentsByEntityType = new DataLoader<{ entityId: string; typeId: string }, ComponentData | null>(
56
+ async (keys: readonly { entityId: string; typeId: string }[]) => {
52
57
  const startTime = Date.now();
53
58
  try {
54
59
  const entityIds = [...new Set(keys.map(k => k.entityId))];
55
60
  const typeIds = [...new Set(keys.map(k => k.typeId))];
56
- const rows = await db`
61
+ const entityIdList = inList(entityIds, 1);
62
+ const typeIdList = inList(typeIds, entityIdList.newParamIndex);
63
+ const rows = await db.unsafe(`
57
64
  SELECT entity_id, type_id, data, created_at, updated_at, deleted_at
58
65
  FROM components
59
- WHERE entity_id IN ${inList(entityIds, 1)}
60
- AND type_id IN ${inList(typeIds, entityIds.length + 1)}
66
+ WHERE entity_id IN ${entityIdList.sql}
67
+ AND type_id IN ${typeIdList.sql}
61
68
  AND deleted_at IS NULL
62
- `;
69
+ `, [...entityIdList.params, ...typeIdList.params]);
63
70
  const map = new Map<string, ComponentData>();
64
71
  rows.forEach((row: any) => {
65
72
  const key = `${row.entity_id}-${row.type_id}`;
@@ -85,5 +92,87 @@ export function createRequestLoaders(db: any): RequestLoaders {
85
92
  }
86
93
  );
87
94
 
88
- return { entityById, componentsByEntityType };
95
+ const relationsByEntityField = new DataLoader<{ entityId: string; relationField: string; relatedType: string; foreignKey?: string }, Entity[]>(
96
+ async (keys: readonly { entityId: string; relationField: string; relatedType: string; foreignKey?: string }[]) => {
97
+ const startTime = Date.now();
98
+ try {
99
+ // Group keys by relation type for efficient querying
100
+ const resultMap = new Map<string, Entity[]>();
101
+
102
+ // For each key, find related entities based on foreign key relationships
103
+ for (const key of keys) {
104
+ let relatedEntities: Entity[] = [];
105
+
106
+ try {
107
+ logger.trace(`[RelationLoader] Looking for ${key.relatedType} entities with foreign key ${key.foreignKey || 'auto-detect'} pointing to ${key.entityId} for field ${key.relationField}`);
108
+
109
+ let whereClause: string;
110
+ if (key.foreignKey) {
111
+ // Use specific foreign key from relation metadata
112
+ whereClause = `(c.data->>'${key.foreignKey}' = $1)`;
113
+ } else {
114
+ // Fallback to common patterns for backward compatibility
115
+ // TODO: Remove this fallback in future versions
116
+ whereClause = `(
117
+ (c.data->>'user_id' = $1) OR
118
+ (c.data->>'parent_id' = $1)
119
+ )`;
120
+ }
121
+
122
+ // Look for entities that have components with foreign keys pointing to our entity
123
+ const rows = await db.unsafe(`
124
+ SELECT DISTINCT c.entity_id, c.data, c.type_id
125
+ FROM components c
126
+ INNER JOIN entities e ON c.entity_id = e.id
127
+ WHERE e.deleted_at IS NULL
128
+ AND c.deleted_at IS NULL
129
+ AND ${whereClause}
130
+ `, [key.entityId]);
131
+
132
+ logger.trace(`[RelationLoader] Found ${rows.length} components with foreign keys pointing to ${key.entityId}`);
133
+ rows.forEach((row: any) => {
134
+ logger.trace(`[RelationLoader] Component ${row.type_id} on entity ${row.entity_id}:`, row.data);
135
+ });
136
+
137
+ // Create Entity objects for each related entity
138
+ const entityIds = [...new Set(rows.map((row: any) => row.entity_id as string))];
139
+ relatedEntities = entityIds.map((id: string) => {
140
+ const entity = new Entity(id);
141
+ entity.setPersisted(true);
142
+ return entity;
143
+ });
144
+
145
+ logger.trace(`[RelationLoader] Created ${relatedEntities.length} related entities for ${key.relationField}`);
146
+
147
+ } catch (queryError) {
148
+ logger.error(`Error querying relations for ${key.entityId}:`);
149
+ logger.error(queryError);
150
+ relatedEntities = [];
151
+ }
152
+
153
+ const mapKey = `${key.entityId}-${key.relationField}-${key.relatedType}`;
154
+ resultMap.set(mapKey, relatedEntities);
155
+ }
156
+
157
+ const duration = Date.now() - startTime;
158
+ if (duration > 1000) {
159
+ logger.warn(`Slow relationsByEntityField query: ${duration}ms for ${keys.length} keys`);
160
+ }
161
+
162
+ return keys.map(k => {
163
+ const mapKey = `${k.entityId}-${k.relationField}-${k.relatedType}`;
164
+ const result = resultMap.get(mapKey) || [];
165
+ logger.trace(`[RelationLoader] Returning ${result.length} entities for ${k.relationField} on ${k.entityId}`);
166
+ return result;
167
+ });
168
+ } catch (error) {
169
+ logger.error(`Error in relationsByEntityField DataLoader:`);
170
+ logger.error(error);
171
+ // Return empty arrays for all keys on error
172
+ return keys.map(() => []);
173
+ }
174
+ }
175
+ );
176
+
177
+ return { entityById, componentsByEntityType, relationsByEntityField };
89
178
  }
@@ -685,10 +685,9 @@ export class SchedulerManager {
685
685
 
686
686
  // Handle excluded components
687
687
  if (componentTarget.excludeComponents && componentTarget.excludeComponents.length > 0) {
688
- // Note: The current Query API might not directly support exclusion
689
- // This would require extending the Query API or using post-filtering
690
- // For now, we'll log a warning and continue
691
- loggerInstance.warn('excludeComponents is not fully supported in scheduled tasks yet. Consider using post-query filtering.');
688
+ for(const component of componentTarget.excludeComponents){
689
+ query = query.without(component);
690
+ }
692
691
  }
693
692
 
694
693
  return query;
@@ -0,0 +1,9 @@
1
+ export interface ArcheTypeMetadata {
2
+ name: string;
3
+ target: Function;
4
+ typeId: string;
5
+ }
6
+
7
+ export interface ArcheTypeFieldOptions {
8
+ nullable?: boolean;
9
+ }
@@ -0,0 +1,16 @@
1
+ export interface ComponentMetadata {
2
+ name: string;
3
+ typeId: string;
4
+ target: Function;
5
+ }
6
+
7
+ export interface ComponentPropertyMetadata {
8
+ propertyKey: string;
9
+ propertyType?: any;
10
+ component_id: string;
11
+ indexed: boolean;
12
+ isPrimitive: boolean;
13
+ isEnum: boolean;
14
+ enumValues?: string[];
15
+ enumKeys?: string[];
16
+ }
@@ -0,0 +1,10 @@
1
+ import {
2
+ GraphQLScalar,
3
+ type GraphQLObject,
4
+ type GraphQLField
5
+ } from "gql/types"
6
+
7
+ export interface GQLObjectMetaData {
8
+ name: string;
9
+ fields: GraphQLField[];
10
+ }
@@ -0,0 +1,14 @@
1
+ import { MetadataStorage } from "./metadata-storage";
2
+
3
+ declare global {
4
+ // eslint-disable-next-line vars-on-top, no-var
5
+ var BunsaneMetadataStorage: MetadataStorage;
6
+ }
7
+
8
+ export function getMetadataStorage(): MetadataStorage {
9
+ if (!global.BunsaneMetadataStorage) {
10
+ global.BunsaneMetadataStorage = new MetadataStorage();
11
+ }
12
+
13
+ return global.BunsaneMetadataStorage;
14
+ }
@@ -0,0 +1,17 @@
1
+ import "reflect-metadata";
2
+ export {getMetadataStorage} from "./getMetadataStorage";
3
+ export function Enum() {
4
+ return (target: any) => {
5
+ Reflect.defineMetadata("isEnum", true, target);
6
+ const staticKeys = Object.getOwnPropertyNames(target).filter(key =>
7
+ key !== 'prototype' &&
8
+ key !== 'length' &&
9
+ key !== 'name' &&
10
+ typeof target[key] !== 'function'
11
+ );
12
+ if (staticKeys.length > 0) {
13
+ Reflect.defineMetadata("__enumValues", staticKeys.map(key => target[key]), target);
14
+ Reflect.defineMetadata("__enumKeys", staticKeys, target);
15
+ }
16
+ };
17
+ }
@@ -0,0 +1,81 @@
1
+ import { createHash } from 'crypto';
2
+ import type {
3
+ ComponentMetadata,
4
+ ComponentPropertyMetadata
5
+ } from "./definitions/Component";
6
+ import type { ArcheTypeMetadata, ArcheTypeFieldOptions } from './definitions/ArcheType';
7
+ import type { RelationOptions } from '../ArcheType';
8
+
9
+ function generateTypeId(name: string): string {
10
+ return createHash('sha256').update(name).digest('hex');
11
+ }
12
+
13
+ type ArcheTypeRelationMap = {fieldName: string, relatedArcheType: new (...args: any[]) => any | string, relationType: 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany', options?: RelationOptions, type?: any}
14
+ type ArcheTypeFieldMap = {fieldName: string, component: new (...args: any[]) => any, options?: ArcheTypeFieldOptions, type?: any};
15
+ type ArcheTypeUnionMap = {fieldName: string, components: (new (...args:any[]) => any)[], options?: ArcheTypeFieldOptions, type?:any};
16
+ export class MetadataStorage {
17
+ components_ids_map: Map<string, string> = new Map();
18
+ components: ComponentMetadata[] = [];
19
+ components_map: Map<string, ComponentMetadata> = new Map();
20
+ componentProperties: Map<string, ComponentPropertyMetadata[]> = new Map();
21
+ archetypes: ArcheTypeMetadata[] = [];
22
+ archetypes_field_map: Map<string, ArcheTypeFieldMap[]> = new Map();
23
+ archetypes_relations_map: Map<string, ArcheTypeRelationMap[]> = new Map();
24
+ archetypes_union_map: Map<string, ArcheTypeUnionMap[]> = new Map();
25
+
26
+
27
+ graphql_types: Map<string, any> = new Map();
28
+
29
+ getComponentId(componentName: string): string {
30
+ if(this.components_ids_map.has(componentName)) {
31
+ return this.components_ids_map.get(componentName)!;
32
+ }
33
+ const typeId = generateTypeId(componentName);
34
+ this.components_ids_map.set(componentName, typeId);
35
+ return typeId;
36
+ }
37
+
38
+ collectComponentMetadata(metadata: ComponentMetadata) {
39
+ this.components.push(metadata);
40
+ this.components_map.set(metadata.name, metadata);
41
+ }
42
+
43
+
44
+ collectComponentPropertyMetadata(metadata: ComponentPropertyMetadata ) {
45
+ if(!this.componentProperties.has(metadata.component_id)) {
46
+ this.componentProperties.set(metadata.component_id, []);
47
+ }
48
+ this.componentProperties.get(metadata.component_id)!.push(metadata);
49
+ }
50
+
51
+
52
+ getComponentProperties(component_id: string): ComponentPropertyMetadata[] {
53
+ return this.componentProperties.get(component_id) || [];
54
+ }
55
+
56
+ collectArchetypeField(archetype_id: string, fieldName: string, component: new (...args: any[]) => any, options?: ArcheTypeFieldOptions, type?: any) {
57
+ if(!this.archetypes_field_map.has(archetype_id)) {
58
+ this.archetypes_field_map.set(archetype_id, []);
59
+ }
60
+ this.archetypes_field_map.get(archetype_id)!.push({fieldName, component, options, type});
61
+ }
62
+
63
+ collectArchetypeUnion(archetype_id: string, fieldName: string, components: (new (...args: any[]) => any)[], options?: ArcheTypeFieldOptions, type?: any) {
64
+ if(!this.archetypes_union_map.has(archetype_id)) {
65
+ this.archetypes_union_map.set(archetype_id, []);
66
+ }
67
+ this.archetypes_union_map.get(archetype_id)!.push({fieldName, components, options, type});
68
+ }
69
+
70
+ collectArchetypeRelation(archetype_id: string, fieldName: string, relatedArcheType: new (...args: any[]) => any | string, relationType: 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany', options?: RelationOptions, type?: any) {
71
+ if(!this.archetypes_relations_map.has(archetype_id)) {
72
+ this.archetypes_relations_map.set(archetype_id, []);
73
+ }
74
+ this.archetypes_relations_map.get(archetype_id)!.push({fieldName, relatedArcheType, relationType, options, type});
75
+ }
76
+
77
+ collectArcheTypeMetadata(metadata: ArcheTypeMetadata) {
78
+ this.archetypes.push(metadata);
79
+ }
80
+ }
81
+
@@ -91,7 +91,6 @@ export const GetDatabaseDataSize = async () => {
91
91
 
92
92
 
93
93
  export const SetupDatabaseExtensions = async () => {
94
- // await db`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`;
95
94
  }
96
95
 
97
96
  export const CreateEntityTable = async () => {
@@ -173,16 +172,17 @@ export const UpdateComponentIndexes = async (table_name: string, indexedProperti
173
172
  }
174
173
  }
175
174
 
176
-
177
- export const CreateComponentPartitionTable = async (comp_name: string, type_id: string, indexedProperties?: string[]) => {
175
+ //TODO: Cleanup and optimize
176
+ export const CreateComponentPartitionTable = async (comp_name: string, type_id: string) => {
178
177
  try {
179
178
  comp_name = validateIdentifier(comp_name);
180
- // type_id is a value, not identifier, so no validation
181
- if (indexedProperties) {
182
- indexedProperties = indexedProperties.map(prop => validateIdentifier(prop));
183
- }
179
+ // // type_id is a value, not identifier, so no validation
180
+ // if (indexedProperties) {
181
+ // indexedProperties = indexedProperties.map(prop => validateIdentifier(prop));
182
+ // }
184
183
  logger.trace(`Attempt adding partition table for component: ${comp_name}`);
185
- const table_name = `components_${comp_name.toLowerCase().replace(/\s+/g, '_')}`;
184
+ // const table_name = `components_${comp_name.toLowerCase().replace(/\s+/g, '_')}`;
185
+ const table_name = GenerateTableName(comp_name);
186
186
  logger.trace(`Checking for existing partition table: ${table_name}`);
187
187
  const existingPartition = await db.unsafe(`SELECT 1 FROM information_schema.tables
188
188
  WHERE table_name = '${table_name}'
@@ -202,16 +202,17 @@ export const CreateComponentPartitionTable = async (comp_name: string, type_id:
202
202
  });
203
203
  logger.trace(`Successfully created partition table: ${table_name}`);
204
204
 
205
- if (BUNSANE_RELATION_TYPED_COLUMN && indexedProperties?.includes('value')) {
206
- logger.trace(`Adding typed FK column for ${table_name}`);
207
- await retryWithBackoff(async () => {
208
- await db.unsafe(`ALTER TABLE ${table_name} ADD COLUMN IF NOT EXISTS fk_id UUID GENERATED ALWAYS AS ((data->>'value')::UUID) STORED`);
209
- });
210
- await retryWithBackoff(async () => {
211
- await db.unsafe(`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_${table_name}_fk_id ON ${table_name} (fk_id)`);
212
- });
213
- logger.trace(`Added fk_id column and index for ${table_name}`);
214
- }
205
+ // TODO: Not sure if this is needed here or should be handled separately
206
+ // if (BUNSANE_RELATION_TYPED_COLUMN && indexedProperties?.includes('value')) {
207
+ // logger.trace(`Adding typed FK column for ${table_name}`);
208
+ // await retryWithBackoff(async () => {
209
+ // await db.unsafe(`ALTER TABLE ${table_name} ADD COLUMN IF NOT EXISTS fk_id UUID GENERATED ALWAYS AS ((data->>'value')::UUID) STORED`);
210
+ // });
211
+ // await retryWithBackoff(async () => {
212
+ // await db.unsafe(`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_${table_name}_fk_id ON ${table_name} (fk_id)`);
213
+ // });
214
+ // logger.trace(`Added fk_id column and index for ${table_name}`);
215
+ // }
215
216
 
216
217
  } catch (error) {
217
218
  logger.error(`Failed to create component partition table for ${comp_name}: ${error}`);
@@ -257,7 +258,8 @@ export const CreateEntityComponentTable = async () => {
257
258
  await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_type_id ON entity_components (type_id);`
258
259
  await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_type_entity ON entity_components (type_id, entity_id);`
259
260
 
260
- // Phase 2A: Add composite indexes for sorting optimization
261
261
  await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_type_entity_deleted ON entity_components (type_id, entity_id, deleted_at);`
262
262
  await db`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_entity_components_deleted_type ON entity_components (deleted_at, type_id) WHERE deleted_at IS NULL;`
263
- }
263
+ }
264
+
265
+ export const GenerateTableName = (name: string) => `components_${name.toLowerCase().replace(/\s+/g, '_')}`;
@@ -1,5 +1,3 @@
1
- import { sql } from 'bun';
2
-
3
1
  export function inList<T>(values: T[], paramIndex: number): { sql: string, params: any[], newParamIndex: number } {
4
2
  if (values.length === 0) return { sql: '()', params: [], newParamIndex: paramIndex };
5
3
  const placeholders = Array.from({length: values.length}, (_, i) => `$${paramIndex + i}`).join(', ');