bunsane 0.1.4 → 0.2.0

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 (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. package/tests/scheduler.test.ts +0 -724
package/core/Query.ts DELETED
@@ -1,880 +0,0 @@
1
- import type { BaseComponent, ComponentDataType } from "./Components";
2
- import { Entity } from "./Entity";
3
- import ComponentRegistry from "./ComponentRegistry";
4
- import { logger } from "./Logger";
5
- import { sql } from "bun";
6
- import db from "database";
7
- import { timed } from "./Decorators";
8
- import { inList } from "../database/sqlHelpers";
9
-
10
- export type FilterOperator = "=" | ">" | "<" | ">=" | "<=" | "!=" | "LIKE" | "IN" | "NOT IN" | string;
11
-
12
- export const FilterOp = {
13
- EQ: "=" as FilterOperator,
14
- GT: ">" as FilterOperator,
15
- LT: "<" as FilterOperator,
16
- GTE: ">=" as FilterOperator,
17
- LTE: "<=" as FilterOperator,
18
- NEQ: "!=" as FilterOperator,
19
- LIKE: "LIKE" as FilterOperator,
20
- IN: "IN" as FilterOperator,
21
- NOT_IN: "NOT IN" as FilterOperator
22
- }
23
- export interface QueryFilter {
24
- field: string;
25
- operator: FilterOperator;
26
- value: any;
27
- }
28
-
29
- export interface QueryFilterOptions {
30
- filters: QueryFilter[];
31
- }
32
-
33
- export type SortDirection = "ASC" | "DESC";
34
-
35
- export interface SortOrder {
36
- component: string;
37
- property: string;
38
- direction: SortDirection;
39
- nullsFirst?: boolean;
40
- }
41
-
42
- class Query {
43
- private requiredComponents: Set<string> = new Set<string>();
44
- private excludedComponents: Set<string> = new Set<string>();
45
- private componentFilters: Map<string, QueryFilter[]> = new Map();
46
- private populateComponents: boolean = false;
47
- private withId: string | null = null;
48
- private limit: number | null = null;
49
- private offsetValue: number = 0;
50
- private eagerComponents: Set<string> = new Set<string>();
51
- private sortOrders: SortOrder[] = [];
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
-
59
- static filterOp = FilterOp;
60
-
61
- public findById(id: string) {
62
- this.withId = id;
63
- return this;
64
- }
65
-
66
- public async findOneById(id: string): Promise<Entity | null> {
67
- const entities = await this.findById(id).exec();
68
- return entities.length > 0 ? entities[0]! : null;
69
- }
70
-
71
- public with<T extends BaseComponent>(ctor: new (...args: any[]) => T, options?: QueryFilterOptions) {
72
- const type_id = ComponentRegistry.getComponentId(ctor.name);
73
- if(!type_id) {
74
- throw new Error(`Component ${ctor.name} is not registered.`);
75
- }
76
- this.requiredComponents.add(type_id);
77
-
78
- if (options?.filters && options.filters.length > 0) {
79
- this.componentFilters.set(type_id, options.filters);
80
- }
81
-
82
- return this;
83
- }
84
-
85
- public eagerLoad<T extends BaseComponent>(ctors: (new (...args: any[]) => T)[]): this {
86
- for (const ctor of ctors) {
87
- const type_id = ComponentRegistry.getComponentId(ctor.name);
88
- if (!type_id) {
89
- throw new Error(`Component ${ctor.name} is not registered.`);
90
- }
91
- this.eagerComponents.add(type_id);
92
- }
93
- return this;
94
- }
95
-
96
- public eagerLoadComponents(ctors: Array<new () => BaseComponent>): this {
97
- for (const ctor of ctors) {
98
- const type_id = ComponentRegistry.getComponentId(ctor.name);
99
- if (!type_id) {
100
- throw new Error(`Component ${ctor.name} is not registered.`);
101
- }
102
- this.eagerComponents.add(type_id);
103
- }
104
- return this;
105
- }
106
-
107
- public static filter(field: string, operator: FilterOperator, value: any): QueryFilter {
108
- return { field, operator, value };
109
- }
110
-
111
- public static typedFilter<T extends BaseComponent>(
112
- componentCtor: new (...args: any[]) => T,
113
- field: keyof ComponentDataType<T>,
114
- operator: FilterOperator,
115
- value: any
116
- ): QueryFilter {
117
- return { field: field as string, operator, value };
118
- }
119
-
120
- public static filters(...filters: QueryFilter[]): QueryFilterOptions {
121
- return { filters };
122
- }
123
-
124
- private buildFilterCondition(filter: QueryFilter, alias: string, paramIndex: number): { sql: string, params: any[], newParamIndex: number } {
125
- const { field, operator, value } = filter;
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
-
132
- // Build JSON path for nested properties (e.g., "parent.child" -> data->'parent'->>'child')
133
- const jsonPath = this.buildJsonPath(field, alias);
134
-
135
- switch (operator) {
136
- case "=":
137
- case ">":
138
- case "<":
139
- case ">=":
140
- case "<=":
141
- case "!=":
142
- if (typeof value === "string") {
143
- return { sql: `${jsonPath} ${operator} $${paramIndex}`, params: [value], newParamIndex: paramIndex + 1 };
144
- } else {
145
- return { sql: `(${jsonPath})::numeric ${operator} $${paramIndex}`, params: [value], newParamIndex: paramIndex + 1 };
146
- }
147
- case "LIKE":
148
- return { sql: `${jsonPath} LIKE $${paramIndex}`, params: [value], newParamIndex: paramIndex + 1 };
149
- case "IN":
150
- if (Array.isArray(value)) {
151
- const placeholders = Array.from({length: value.length}, (_, i) => `$${paramIndex + i}`).join(', ');
152
- return { sql: `${jsonPath} IN (${placeholders})`, params: value, newParamIndex: paramIndex + value.length };
153
- }
154
- throw new Error("IN operator requires an array of values");
155
- case "NOT IN":
156
- if (Array.isArray(value)) {
157
- const placeholders = Array.from({length: value.length}, (_, i) => `$${paramIndex + i}`).join(', ');
158
- return { sql: `${jsonPath} NOT IN (${placeholders})`, params: value, newParamIndex: paramIndex + value.length };
159
- }
160
- throw new Error("NOT IN operator requires an array of values");
161
- default:
162
- throw new Error(`Unsupported operator: ${operator}`);
163
- }
164
- }
165
-
166
- /**
167
- * Build PostgreSQL JSON path expression for nested properties
168
- * @param field Field path (e.g., "parent.child.grandchild")
169
- * @param alias Table alias for the components table
170
- * @returns PostgreSQL JSON path expression
171
- */
172
- private buildJsonPath(field: string, alias: string): string {
173
- const parts = field.split('.');
174
-
175
- if (parts.length === 1) {
176
- // Single level: data->>'field'
177
- return `${alias}.data->>'${field}'`;
178
- } else {
179
- // Nested levels: data->'parent'->'child'->>'grandchild'
180
- const pathParts = parts.slice(0, -1).map(part => `'${part}'`);
181
- const lastPart = parts[parts.length - 1];
182
-
183
- return `${alias}.data->${pathParts.join('->')}->>'${lastPart}'`;
184
- }
185
- }
186
-
187
- private buildFilterWhereClause(typeId: string, filters: QueryFilter[], alias: string, paramIndex: number): { sql: string, params: any[], newParamIndex: number } {
188
- if (filters.length === 0) return { sql: '', params: [], newParamIndex: paramIndex };
189
-
190
- const conditions: string[] = [];
191
- const allParams: any[] = [];
192
- let currentIndex = paramIndex;
193
- for (const filter of filters) {
194
- const { sql, params, newParamIndex } = this.buildFilterCondition(filter, alias, currentIndex);
195
- conditions.push(sql);
196
- allParams.push(...params);
197
- currentIndex = newParamIndex;
198
- }
199
- const sql = conditions.join(' AND ');
200
- return { sql, params: allParams, newParamIndex: currentIndex };
201
- }
202
-
203
-
204
- public without<T extends BaseComponent>(ctor: new (...args: any[]) => T) {
205
- const type_id = ComponentRegistry.getComponentId(ctor.name);
206
- if(!type_id) {
207
- throw new Error(`Component ${ctor.name} is not registered.`);
208
- }
209
- this.excludedComponents.add(type_id);
210
- return this;
211
- }
212
-
213
- public populate(): this {
214
- this.populateComponents = true;
215
- return this;
216
- }
217
-
218
- public take(limit: number): this {
219
- this.limit = limit;
220
- return this;
221
- }
222
-
223
- public offset(offset: number): this {
224
- this.offsetValue = offset;
225
- return this;
226
- }
227
-
228
- public sortBy<T extends BaseComponent>(
229
- componentCtor: new (...args: any[]) => T,
230
- property: keyof ComponentDataType<T>,
231
- direction: SortDirection = "ASC",
232
- nullsFirst: boolean = false
233
- ): this {
234
- const componentName = componentCtor.name;
235
- const typeId = ComponentRegistry.getComponentId(componentName);
236
-
237
- if (!typeId) {
238
- throw new Error(`Component ${componentName} is not registered.`);
239
- }
240
-
241
- // Validate that the component is required in this query
242
- if (!this.requiredComponents.has(typeId)) {
243
- throw new Error(`Cannot sort by component ${componentName} that is not included in the query. Use .with(${componentName}) first.`);
244
- }
245
-
246
- this.sortOrders.push({
247
- component: componentName,
248
- property: property as string,
249
- direction,
250
- nullsFirst
251
- });
252
-
253
- return this;
254
- }
255
-
256
- public orderBy(orders: SortOrder[]): this {
257
- // Validate each sort order
258
- for (const order of orders) {
259
- const typeId = ComponentRegistry.getComponentId(order.component);
260
- if (!typeId) {
261
- throw new Error(`Component ${order.component} is not registered.`);
262
- }
263
- if (!this.requiredComponents.has(typeId)) {
264
- throw new Error(`Cannot sort by component ${order.component} that is not included in the query. Use .with(${order.component}) first.`);
265
- }
266
- }
267
-
268
- this.sortOrders = orders;
269
- return this;
270
- }
271
-
272
- public count(): Promise<number> {
273
- return this.doCount();
274
- }
275
-
276
- @timed("Query.exec")
277
- public async exec(): Promise<Entity[]> {
278
- return new Promise<Entity[]>((resolve, reject) => {
279
- // Add timeout to prevent hanging queries
280
- const timeout = setTimeout(() => {
281
- logger.error(`Query execution timeout`);
282
- reject(new Error(`Query execution timeout after 30 seconds`));
283
- }, 30000); // 30 second timeout
284
-
285
- this.doExec()
286
- .then(result => {
287
- clearTimeout(timeout);
288
- resolve(result);
289
- })
290
- .catch(error => {
291
- clearTimeout(timeout);
292
- reject(error);
293
- });
294
- });
295
- }
296
-
297
- private async doExec(): Promise<Entity[]> {
298
- const componentIds = Array.from(this.requiredComponents);
299
- const excludedIds = Array.from(this.excludedComponents);
300
- const componentCount = componentIds.length;
301
- const hasRequired = componentCount > 0;
302
- const hasExcluded = excludedIds.length > 0;
303
- const hasFilters = this.componentFilters.size > 0;
304
- const hasWithId = this.withId !== null;
305
-
306
- let ids: string[] = [];
307
-
308
- switch (true) {
309
- case !hasRequired && !hasExcluded && !hasWithId:
310
- return [];
311
- case !hasRequired && !hasExcluded && hasWithId:
312
- let query = db`SELECT id FROM entities WHERE id = ${this.withId} AND deleted_at IS NULL ORDER BY id`;
313
- if (this.limit !== null) {
314
- query = db`${query} LIMIT ${this.limit}`;
315
- }
316
- if (this.offsetValue > 0) {
317
- query = db`${query} OFFSET ${this.offsetValue}`;
318
- }
319
- const result = await query;
320
- ids = result.map((row: any) => row.id);
321
- break;
322
- case hasRequired && hasExcluded && hasFilters:
323
- ids = await this.getIdsWithFiltersAndExclusions(componentIds, excludedIds, componentCount, this.limit, this.offsetValue);
324
- break;
325
- case hasRequired && hasExcluded:
326
- const componentIdsString = inList(componentIds, 1);
327
- const excludedIdsString = inList(excludedIds, componentIdsString.newParamIndex);
328
- let excludedQuery = db`
329
- SELECT ec.entity_id as id
330
- FROM entity_components ec
331
- WHERE ec.type_id IN ${db.unsafe(componentIdsString.sql, componentIdsString.params)} AND ec.deleted_at IS NULL
332
- ${this.withId ? db`AND ec.entity_id = ${this.withId}` : db``}
333
- AND NOT EXISTS (
334
- SELECT 1 FROM entity_components ec_ex
335
- 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
336
- )
337
- GROUP BY ec.entity_id
338
- HAVING COUNT(DISTINCT ec.type_id) = ${componentCount}
339
- ORDER BY ec.entity_id
340
- `;
341
- if (this.limit !== null) {
342
- excludedQuery = db`${excludedQuery} LIMIT ${this.limit}`;
343
- }
344
- if (this.offsetValue > 0) {
345
- excludedQuery = db`${excludedQuery} OFFSET ${this.offsetValue}`;
346
- }
347
- const excludedQueryResult = await excludedQuery;
348
- ids = excludedQueryResult.map((row: any) => row.id);
349
- break;
350
- case hasRequired && hasFilters:
351
- ids = await this.getIdsWithFilters(componentIds, componentCount, this.limit, this.offsetValue);
352
- break;
353
- case hasRequired:
354
- let queryStr: any;
355
- let requiredOnlyQueryResult: any;
356
- if (componentCount === 1) {
357
- if (this.sortOrders.length > 0) {
358
- const typeId = componentIds[0]!;
359
- const sortExpression = this.buildSortExpressionForSingleComponent(typeId, "c");
360
- queryStr = db`SELECT DISTINCT ec.entity_id as id ${db.unsafe(sortExpression.select)} FROM entity_components ec JOIN components c ON ec.entity_id = c.entity_id AND c.type_id = ${typeId} AND c.deleted_at IS NULL WHERE ec.type_id = ${typeId} ${this.withId ? db`AND ec.entity_id = ${this.withId}` : db``} AND ec.deleted_at IS NULL ${db.unsafe(sortExpression.orderBy)}`;
361
- } else {
362
- queryStr = db`SELECT entity_id as id FROM entity_components WHERE type_id = ${componentIds[0]} ${this.withId ? db`AND entity_id = ${this.withId}` : db``} AND deleted_at IS NULL ORDER BY entity_id`;
363
- }
364
- if (this.limit !== null) {
365
- queryStr = db`${queryStr} LIMIT ${this.limit}`;
366
- }
367
- if (this.offsetValue > 0) {
368
- queryStr = db`${queryStr} OFFSET ${this.offsetValue}`;
369
- }
370
- requiredOnlyQueryResult = await queryStr;
371
- } else {
372
- // Phase 2A: Optimize multi-component sorting with JOINs instead of subqueries
373
- if (this.sortOrders.length > 0) {
374
- const compIds = inList(componentIds, 1);
375
- let orderByClause = "ORDER BY ";
376
- const orderClauses: string[] = [];
377
-
378
- for (const order of this.sortOrders) {
379
- const typeId = ComponentRegistry.getComponentId(order.component);
380
- if (typeId && componentIds.includes(typeId)) {
381
- const direction = order.direction.toUpperCase();
382
- const nullsClause = order.nullsFirst ? "NULLS FIRST" : "NULLS LAST";
383
- // Use JOIN-based sorting instead of subquery
384
- const subquery = `(SELECT (c.data->>'${order.property}')::numeric FROM components c WHERE c.entity_id = base_query.id AND c.type_id = '${typeId}' AND c.deleted_at IS NULL LIMIT 1)`;
385
- orderClauses.push(`${subquery} ${direction} ${nullsClause}`);
386
- }
387
- }
388
- orderClauses.push("base_query.id ASC");
389
- orderByClause += orderClauses.join(", ");
390
-
391
- queryStr = db`SELECT * FROM (SELECT DISTINCT entity_id as id FROM entity_components WHERE type_id IN ${db.unsafe(compIds.sql, compIds.params)} ${this.withId ? db`AND entity_id = ${this.withId}` : db``} AND deleted_at IS NULL GROUP BY entity_id HAVING COUNT(DISTINCT type_id) = ${componentCount}) base_query ${db.unsafe(orderByClause)}`;
392
- } else {
393
- const compIds = inList(componentIds, 1);
394
- queryStr = db`SELECT DISTINCT entity_id as id FROM entity_components WHERE type_id IN ${db.unsafe(compIds.sql, compIds.params)} ${this.withId ? db`AND entity_id = ${this.withId}` : db``} AND deleted_at IS NULL GROUP BY entity_id HAVING COUNT(DISTINCT type_id) = ${componentCount} ORDER BY entity_id`;
395
- }
396
- if (this.limit !== null) {
397
- queryStr = db`${queryStr} LIMIT ${this.limit}`;
398
- }
399
- if (this.offsetValue > 0) {
400
- queryStr = db`${queryStr} OFFSET ${this.offsetValue}`;
401
- }
402
- requiredOnlyQueryResult = await queryStr;
403
- }
404
- ids = requiredOnlyQueryResult.map((row: any) => row.id);
405
- break;
406
- case hasExcluded:
407
- const onlyExcludedIdsString = inList(excludedIds, 1);
408
- let onlyExcludedQuery = db`
409
- SELECT DISTINCT ec.entity_id as id
410
- FROM entity_components ec
411
- WHERE ${this.withId ? db`ec.entity_id = ${this.withId} AND ` : db``} NOT EXISTS (
412
- SELECT 1 FROM entity_components ec_ex
413
- 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
414
- )
415
- AND ec.deleted_at IS NULL
416
- ORDER BY ec.entity_id
417
- `;
418
- if (this.limit !== null) {
419
- onlyExcludedQuery = db`${onlyExcludedQuery} LIMIT ${this.limit}`;
420
- }
421
- if (this.offsetValue > 0) {
422
- onlyExcludedQuery = db`${onlyExcludedQuery} OFFSET ${this.offsetValue}`;
423
- }
424
- const onlyExcludedQueryResult = await onlyExcludedQuery;
425
- ids = onlyExcludedQueryResult.map((row: any) => row.id);
426
- break;
427
- default:
428
- return [];
429
- }
430
-
431
- if (this.populateComponents) {
432
- return await Entity.LoadMultiple(ids);
433
- } else {
434
- const len = ids.length;
435
- const entities = new Array(len);
436
- for (let i = 0; i < len; i += 4) {
437
- if (i < len) {
438
- const entity = new Entity(ids[i]);
439
- entity.setPersisted(true);
440
- entity.setDirty(false);
441
- entities[i] = entity;
442
- }
443
- if (i + 1 < len) {
444
- const entity = new Entity(ids[i + 1]);
445
- entity.setPersisted(true);
446
- entity.setDirty(false);
447
- entities[i + 1] = entity;
448
- }
449
- if (i + 2 < len) {
450
- const entity = new Entity(ids[i + 2]);
451
- entity.setPersisted(true);
452
- entity.setDirty(false);
453
- entities[i + 2] = entity;
454
- }
455
- if (i + 3 < len) {
456
- const entity = new Entity(ids[i + 3]);
457
- entity.setPersisted(true);
458
- entity.setDirty(false);
459
- entities[i + 3] = entity;
460
- }
461
- }
462
- if (this.eagerComponents.size > 0) {
463
- await Entity.LoadComponents(entities, Array.from(this.eagerComponents));
464
- }
465
- return entities;
466
- }
467
- }
468
-
469
- private buildOrderByClause(): string {
470
- if (this.sortOrders.length === 0) {
471
- return 'ORDER BY ec.entity_id';
472
- }
473
-
474
- const orderClauses: string[] = [];
475
- for (const order of this.sortOrders) {
476
- const typeId = ComponentRegistry.getComponentId(order.component);
477
- if (!typeId) continue;
478
-
479
- // For now, assume we have a component alias. In practice, we'd need to map component types to aliases
480
- // This is a simplified implementation - in a full implementation, we'd need to track aliases per component
481
- const componentAlias = `c_${typeId}`;
482
- const direction = order.direction.toUpperCase();
483
- const nulls = order.nullsFirst ? 'NULLS FIRST' : 'NULLS LAST';
484
-
485
- // Use buildJsonPath for nested property support
486
- const jsonPath = this.buildJsonPath(order.property, componentAlias);
487
- orderClauses.push(`(${jsonPath})::text ${direction} ${nulls}`);
488
- }
489
-
490
- // Always include entity_id as final tiebreaker for consistent ordering
491
- orderClauses.push('ec.entity_id ASC');
492
-
493
- return `ORDER BY ${orderClauses.join(', ')}`;
494
- }
495
-
496
- private buildOrderByClauseWithJoinData(componentIds: string[], joinCount: number): string {
497
- if (this.sortOrders.length === 0) {
498
- return `ORDER BY id`;
499
- }
500
-
501
- const orderClauses: string[] = [];
502
-
503
- for (let i = 0; i < this.sortOrders.length; i++) {
504
- const order = this.sortOrders[i];
505
- if (!order) continue;
506
-
507
- const typeId = ComponentRegistry.getComponentId(order.component);
508
- if (!typeId || !componentIds.includes(typeId)) {
509
- continue; // Skip if component not in query
510
- }
511
-
512
- const direction = order.direction.toUpperCase();
513
- const nullsClause = order.nullsFirst ? "NULLS FIRST" : "NULLS LAST";
514
-
515
- // For subquery approach, use correlated subquery for sorting
516
- const sortExpression = `(SELECT (c.data->>'${order.property}')::numeric FROM components c WHERE c.entity_id = filtered_entities.id AND c.type_id = '${typeId}' AND c.deleted_at IS NULL LIMIT 1)`;
517
-
518
- orderClauses.push(`${sortExpression} ${direction} ${nullsClause}`);
519
- }
520
-
521
- // Always include entity_id as final tiebreaker for consistent ordering
522
- orderClauses.push(`id ASC`);
523
-
524
- return `ORDER BY ${orderClauses.join(", ")}`;
525
- }
526
-
527
- private buildSortExpressionForSingleComponent(typeId: string, alias: string): { select: string, orderBy: string } {
528
- if (this.sortOrders.length === 0) {
529
- return { select: "", orderBy: "ORDER BY ec.entity_id" };
530
- }
531
-
532
- const order = this.sortOrders[0]; // For single component, we only support single sort
533
- if (!order) {
534
- return { select: "", orderBy: "ORDER BY ec.entity_id" };
535
- }
536
-
537
- const direction = order.direction.toUpperCase();
538
- const nullsClause = order.nullsFirst ? "NULLS FIRST" : "NULLS LAST";
539
-
540
- // Use buildJsonPath for nested property support
541
- const jsonPath = this.buildJsonPath(order.property, alias);
542
- const selectExpr = `, (${jsonPath})::numeric as sort_val`;
543
- const orderByExpr = `ORDER BY sort_val ${direction} ${nullsClause}, ec.entity_id ASC`;
544
-
545
- return { select: selectExpr, orderBy: orderByExpr };
546
- }
547
-
548
- private async getIdsWithFilters(componentIds: string[], componentCount: number, limit?: number | null, offset?: number): Promise<string[]> {
549
- let params: any[] = [];
550
- let paramIndex = 1;
551
- const compIds = inList(componentIds, paramIndex);
552
- params.push(...compIds.params);
553
- paramIndex = compIds.newParamIndex;
554
-
555
- const joins: string[] = [];
556
- let joinIndex = 0;
557
- for (const [typeId, filters] of this.componentFilters.entries()) {
558
- if (componentIds.includes(typeId)) {
559
- const alias = `c${joinIndex}`;
560
- joins.push(`JOIN components ${alias} ON ec.entity_id = ${alias}.entity_id AND ${alias}.type_id = $${paramIndex} AND ${alias}.deleted_at IS NULL`);
561
- params.push(typeId);
562
- paramIndex++;
563
- joinIndex++;
564
- }
565
- }
566
-
567
- let sql: string;
568
-
569
- // For sorting, use a CTE approach to avoid GROUP BY conflicts
570
- if (this.sortOrders.length > 0) {
571
- let selectColumns = `ec.entity_id as id`;
572
-
573
- // Add sort columns using window functions
574
- const sortColumns: string[] = [];
575
- for (let i = 0; i < this.sortOrders.length; i++) {
576
- const order = this.sortOrders[i];
577
- if (!order) continue;
578
-
579
- const typeId = ComponentRegistry.getComponentId(order.component);
580
- if (!typeId || !componentIds.includes(typeId)) {
581
- continue; // Skip if component not in query
582
- }
583
-
584
- // Find the join alias for this component
585
- let sortAlias = '';
586
- let aliasIndex = 0;
587
- for (const [filterTypeId, filters] of Array.from(this.componentFilters.entries())) {
588
- if (componentIds.includes(filterTypeId) && filterTypeId === typeId) {
589
- sortAlias = `c${aliasIndex}`;
590
- break;
591
- }
592
- if (componentIds.includes(filterTypeId)) {
593
- aliasIndex++;
594
- }
595
- }
596
-
597
- if (sortAlias) {
598
- sortColumns.push(`FIRST_VALUE((${sortAlias}.data->>'${order.property}')::numeric) OVER (PARTITION BY ec.entity_id ORDER BY ${sortAlias}.created_at DESC) as sort_val_${i}`);
599
- }
600
- }
601
- if (sortColumns.length > 0) {
602
- selectColumns += `, ${sortColumns.join(', ')}`;
603
- }
604
-
605
- // Use CTE to get filtered entities with sort values
606
- sql = `WITH filtered_entities AS (
607
- SELECT ${selectColumns}
608
- FROM entity_components ec ${joins.join(' ')}
609
- WHERE ec.type_id IN ${compIds.sql} AND ec.deleted_at IS NULL`;
610
-
611
- if (this.withId) {
612
- sql += ` AND ec.entity_id = $${paramIndex}`;
613
- params.push(this.withId);
614
- paramIndex++;
615
- }
616
-
617
- joinIndex = 0;
618
- for (const [typeId, filters] of this.componentFilters.entries()) {
619
- if (componentIds.includes(typeId)) {
620
- const alias = `c${joinIndex}`;
621
- const filterConditions = this.buildFilterWhereClause(typeId, filters, alias, paramIndex);
622
- if (filterConditions.sql) {
623
- sql += ` AND ${filterConditions.sql}`;
624
- params.push(...filterConditions.params);
625
- paramIndex = filterConditions.newParamIndex;
626
- }
627
- joinIndex++;
628
- }
629
- }
630
-
631
- sql += `
632
- )
633
- SELECT DISTINCT fe.id, ${sortColumns.length > 0 ? sortColumns.map((_, i) => `fe.sort_val_${i}`).join(', ') : ''}
634
- FROM filtered_entities fe
635
- WHERE (SELECT COUNT(DISTINCT ec.type_id) FROM entity_components ec WHERE ec.entity_id = fe.id AND ec.deleted_at IS NULL) = $${paramIndex}`;
636
-
637
- params.push(componentCount);
638
- paramIndex++;
639
-
640
- // Build ORDER BY clause using the selected sort values
641
- const orderClauses: string[] = [];
642
- for (let i = 0; i < this.sortOrders.length; i++) {
643
- const order = this.sortOrders[i];
644
- if (!order) continue;
645
-
646
- const direction = order.direction.toUpperCase();
647
- const nullsClause = order.nullsFirst ? "NULLS FIRST" : "NULLS LAST";
648
- orderClauses.push(`sort_val_${i} ${direction} ${nullsClause}`);
649
- }
650
- // Always include entity_id as final tiebreaker for consistent ordering
651
- orderClauses.push(`id ASC`);
652
- sql += ` ORDER BY ${orderClauses.join(", ")}`;
653
- } else {
654
- // No sorting - use simpler approach
655
- sql = `SELECT DISTINCT ec.entity_id as id FROM entity_components ec ${joins.join(' ')} WHERE ec.type_id IN ${compIds.sql} AND ec.deleted_at IS NULL`;
656
-
657
- if (this.withId) {
658
- sql += ` AND ec.entity_id = $${paramIndex}`;
659
- params.push(this.withId);
660
- paramIndex++;
661
- }
662
-
663
- joinIndex = 0;
664
- for (const [typeId, filters] of this.componentFilters.entries()) {
665
- if (componentIds.includes(typeId)) {
666
- const alias = `c${joinIndex}`;
667
- const filterConditions = this.buildFilterWhereClause(typeId, filters, alias, paramIndex);
668
- if (filterConditions.sql) {
669
- sql += ` AND ${filterConditions.sql}`;
670
- params.push(...filterConditions.params);
671
- paramIndex = filterConditions.newParamIndex;
672
- }
673
- joinIndex++;
674
- }
675
- }
676
-
677
- sql += ` GROUP BY ec.entity_id HAVING COUNT(DISTINCT ec.type_id) = $${paramIndex}`;
678
- params.push(componentCount);
679
- paramIndex++;
680
- sql += ` ORDER BY ec.entity_id`;
681
- }
682
-
683
- if (limit !== null && limit !== undefined) {
684
- sql += ` LIMIT $${paramIndex}`;
685
- params.push(limit);
686
- paramIndex++;
687
- }
688
- if (offset && offset > 0) {
689
- sql += ` OFFSET $${paramIndex}`;
690
- params.push(offset);
691
- paramIndex++;
692
- }
693
-
694
- const filteredResult = await db.unsafe(sql, params);
695
- return filteredResult.map((row: any) => row.id);
696
- }
697
-
698
- private async getIdsWithFiltersAndExclusions(componentIds: string[], excludedIds: string[], componentCount: number, limit?: number | null, offset?: number): Promise<string[]> {
699
- const entityIds = await this.getIdsWithFilters(componentIds, componentCount);
700
-
701
- if (entityIds.length === 0) {
702
- return [];
703
- }
704
-
705
- const idsList = sql(entityIds);
706
- const excludedList = inList(excludedIds, 1);
707
- let query = db`
708
- WITH entity_list AS (
709
- SELECT unnest(${idsList}) as id
710
- )
711
- SELECT el.id
712
- FROM entity_list el
713
- WHERE NOT EXISTS (
714
- SELECT 1 FROM entity_components ec
715
- WHERE ec.entity_id = el.id AND ec.type_id IN ${db.unsafe(excludedList.sql, excludedList.params)} AND ec.deleted_at IS NULL
716
- )
717
- ORDER BY el.id
718
- `;
719
- if (limit !== null && limit !== undefined) {
720
- query = db`${query} LIMIT ${limit}`;
721
- }
722
- if (offset && offset > 0) {
723
- query = db`${query} OFFSET ${offset}`;
724
- }
725
- const exclusionResult = await query;
726
- return exclusionResult.map((row: any) => row.id);
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
- }
878
- }
879
-
880
- export default Query;