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
@@ -1,5 +1,5 @@
1
1
  import type { Entity } from "./Entity";
2
- import type { BaseComponent } from "./Components";
2
+ import { type BaseComponent } from "./components";
3
3
  import ArcheType from "./ArcheType";
4
4
  import {
5
5
  EntityLifecycleEvent,
@@ -1,4 +1,6 @@
1
+ import type { SQL } from "bun";
2
+
1
3
  export interface IEntity {
2
- doSave(): Promise<boolean>;
4
+ save(trx?: SQL): Promise<boolean>;
3
5
  doDelete(force?: boolean): Promise<boolean>;
4
6
  }
@@ -15,18 +15,6 @@ class EntityManager {
15
15
  });
16
16
  }
17
17
 
18
- public saveEntity(entity: IEntity) {
19
- return new Promise<boolean>(async resolve => {
20
- if(!this.dbReady) {
21
- this.entityQueue.push(entity);
22
- return resolve(true);
23
- } else {
24
- const result = await entity.doSave();
25
- resolve(result);
26
- }
27
- })
28
- }
29
-
30
18
  public deleteEntity(entity: IEntity, force: boolean = false) {
31
19
  return new Promise<boolean>(async resolve => {
32
20
  if(!this.dbReady) {
@@ -40,7 +28,7 @@ class EntityManager {
40
28
  private async savePendingEntities() {
41
29
  const promiseWait = [];
42
30
  for(const entity of this.entityQueue) {
43
- promiseWait.push(entity.doSave());
31
+ promiseWait.push(entity.save());
44
32
  }
45
33
  return await Promise.all(promiseWait);
46
34
  }
@@ -37,12 +37,18 @@ export function handleGraphQLError(err: any): never {
37
37
  if (err instanceof z.ZodError) {
38
38
  // Convert Zod errors to user-friendly messages
39
39
  const userFriendlyErrors = err.issues.map((issue: any) => {
40
+ // Use custom Zod message if available, otherwise use mapped error
41
+ const customMessage = issue.message && issue.message !== 'Required' && issue.message !== 'Invalid'
42
+ ? issue.message
43
+ : null;
44
+
40
45
  const errorCode = mapZodPathToErrorCode(issue.path);
41
46
  const errorInfo = getErrorMessage(errorCode);
47
+
42
48
  return {
43
49
  field: issue.path.join('.'),
44
- message: errorInfo.userMessage,
45
- suggestion: errorInfo.suggestion,
50
+ message: customMessage || errorInfo.userMessage,
51
+ suggestion: customMessage ? undefined : errorInfo.suggestion,
46
52
  code: errorCode
47
53
  };
48
54
  });
package/core/Logger.ts CHANGED
@@ -3,6 +3,15 @@ import pino from "pino";
3
3
  const usePretty = process.env.LOG_PRETTY === 'true';
4
4
  export const logger = pino({
5
5
  level: process.env.LOG_LEVEL || 'info',
6
+ redact: {
7
+ paths: [
8
+ 'password', 'secret', 'token', 'authorization',
9
+ 'config.password', '*.password', '*.secret', '*.token',
10
+ '*.accessKeyId', '*.secretAccessKey', '*.sessionToken',
11
+ 'accessKeyId', 'secretAccessKey', 'sessionToken',
12
+ ],
13
+ censor: '[REDACTED]',
14
+ },
6
15
  ...(usePretty && {
7
16
  transport: {
8
17
  target: 'pino-pretty',
@@ -0,0 +1,34 @@
1
+ import { logger as MainLogger } from './Logger';
2
+ const logger = MainLogger.child({ scope: 'Middleware' });
3
+
4
+ export type MiddlewareNext = () => Promise<Response>;
5
+ export type Middleware = (req: Request, next: MiddlewareNext) => Promise<Response>;
6
+
7
+ /**
8
+ * Composes an array of middleware into a single handler function.
9
+ * Each middleware wraps the next, forming an onion-style execution chain.
10
+ */
11
+ export function composeMiddleware(
12
+ middlewares: Middleware[],
13
+ finalHandler: (req: Request) => Promise<Response>,
14
+ ): (req: Request) => Promise<Response> {
15
+ return (req: Request) => {
16
+ let index = -1;
17
+
18
+ function dispatch(i: number): Promise<Response> {
19
+ if (i <= index) {
20
+ return Promise.reject(new Error('next() called multiple times'));
21
+ }
22
+ index = i;
23
+
24
+ if (i >= middlewares.length) {
25
+ return finalHandler(req);
26
+ }
27
+
28
+ const middleware = middlewares[i]!;
29
+ return middleware(req, () => dispatch(i + 1));
30
+ }
31
+
32
+ return dispatch(0);
33
+ };
34
+ }
@@ -2,12 +2,14 @@ import type { Plugin } from 'graphql-yoga';
2
2
  import { createRequestLoaders } from './RequestLoaders';
3
3
  import type { RequestLoaders } from './RequestLoaders';
4
4
  import db from '../database';
5
+ import { CacheManager } from './cache/CacheManager';
5
6
 
6
7
  declare module 'graphql-yoga' {
7
8
  interface Context {
8
9
  locals: {
9
10
  loaders: RequestLoaders;
10
11
  requestId: string;
12
+ cacheManager: CacheManager;
11
13
  };
12
14
  }
13
15
  }
@@ -15,9 +17,11 @@ declare module 'graphql-yoga' {
15
17
  export function createRequestContextPlugin(): Plugin {
16
18
  return {
17
19
  onExecute: ({ args }) => {
20
+ const cacheManager = CacheManager.getInstance();
18
21
  (args as any).contextValue.locals = {
19
- loaders: createRequestLoaders(db),
22
+ loaders: createRequestLoaders(db, cacheManager),
20
23
  requestId: crypto.randomUUID(),
24
+ cacheManager: cacheManager,
21
25
  };
22
26
  },
23
27
  };
@@ -5,8 +5,11 @@ import { inList } from '../database/sqlHelpers';
5
5
  import {logger as MainLogger} from './Logger';
6
6
  const logger = MainLogger.child({ module: 'RequestLoaders' });
7
7
  import { getMetadataStorage } from './metadata';
8
+ import type { CacheManager } from './cache/CacheManager';
8
9
 
9
10
  export type ComponentData = {
11
+ id: string; // Component ID for updates
12
+ entityId: string; // Entity ID
10
13
  typeId: string;
11
14
  data: any;
12
15
  createdAt: Date;
@@ -20,75 +23,177 @@ export type RequestLoaders = {
20
23
  relationsByEntityField: DataLoader<{ entityId: string; relationField: string; relatedType: string; foreignKey?: string }, Entity[]>;
21
24
  };
22
25
 
23
- export function createRequestLoaders(db: any): RequestLoaders {
26
+ export function createRequestLoaders(db: any, cacheManager?: CacheManager): RequestLoaders {
24
27
  const entityById = new DataLoader<string, Entity | null>(async (ids: readonly string[]) => {
25
28
  const startTime = Date.now();
26
29
  try {
27
- const uniqueIds = [...new Set(ids)];
28
- const idList = inList(uniqueIds, 1);
29
- const rows = await db.unsafe(`
30
- SELECT id
31
- FROM entities
32
- WHERE id IN ${idList.sql}
33
- AND deleted_at IS NULL
34
- `, idList.params);
35
- const entities = rows.map((row: any) => {
36
- const entity = new Entity(row.id);
37
- entity.setPersisted(true);
38
- return entity;
39
- });
40
- const map = new Map<string, Entity>();
41
- entities.forEach((e: Entity) => map.set(e.id, e));
30
+ // Filter out empty/invalid IDs to prevent PostgreSQL UUID parsing errors
31
+ const validIds = ids.filter(id => id && typeof id === 'string' && id.trim() !== '');
32
+ if (validIds.length === 0) {
33
+ return ids.map(() => null);
34
+ }
35
+
36
+ const uniqueIds = [...new Set(validIds)];
37
+ const results = new Map<string, Entity | null>();
38
+
39
+ // Note: Entity cache now only tracks existence, not full entity data
40
+ // Full entities are always loaded from database for component access
41
+
42
+ // Find missing entities that weren't in cache
43
+ const missingIds = uniqueIds.filter(id => !results.has(id));
42
44
 
45
+ if (missingIds.length > 0) {
46
+ const idList = inList(missingIds, 1);
47
+ const rows = await db.unsafe(`
48
+ SELECT id
49
+ FROM entities
50
+ WHERE id IN ${idList.sql}
51
+ AND deleted_at IS NULL
52
+ `, idList.params);
53
+
54
+ const entities = rows.map((row: any) => {
55
+ const entity = new Entity(row.id);
56
+ entity.setPersisted(true);
57
+ return entity;
58
+ });
59
+
60
+ // Cache the loaded entities if cache is enabled
61
+ if (cacheManager && cacheManager.getConfig().enabled && cacheManager.getConfig().entity?.enabled) {
62
+ try {
63
+ await cacheManager.setEntitiesWriteThrough(entities, cacheManager.getConfig().entity!.ttl);
64
+ } catch (error) {
65
+ logger.warn({ scope: 'cache', component: 'RequestLoaders', msg: 'Cache write failed for entities', error });
66
+ }
67
+ }
68
+
69
+ entities.forEach((e: Entity) => results.set(e.id, e));
70
+ }
71
+
43
72
  const duration = Date.now() - startTime;
44
73
  if (duration > 1000) { // Log slow queries
45
- console.warn(`Slow entityById query: ${duration}ms for ${ids.length} entities`);
74
+ logger.warn(`Slow entityById query: ${duration}ms for ${ids.length} entities`);
46
75
  }
47
76
 
48
- return ids.map(id => map.get(id) ?? null);
49
- } catch (error) {
50
- console.error(`Error in entityById DataLoader:`, error);
77
+ // Return null for invalid IDs
78
+ return ids.map(id => {
79
+ if (!id || typeof id !== 'string' || id.trim() === '') return null;
80
+ return results.get(id) ?? null;
81
+ });
82
+ } catch (error: any) {
83
+ logger.error(`Error in entityById DataLoader:`, error);
51
84
  throw error;
52
85
  }
86
+ }, {
87
+ maxBatchSize: 100 // Prevent extremely large batches
53
88
  });
54
89
 
55
90
  const componentsByEntityType = new DataLoader<{ entityId: string; typeId: string }, ComponentData | null>(
56
91
  async (keys: readonly { entityId: string; typeId: string }[]) => {
57
92
  const startTime = Date.now();
58
93
  try {
59
- const entityIds = [...new Set(keys.map(k => k.entityId))];
60
- const typeIds = [...new Set(keys.map(k => k.typeId))];
61
- const entityIdList = inList(entityIds, 1);
62
- const typeIdList = inList(typeIds, entityIdList.newParamIndex);
63
- const rows = await db.unsafe(`
64
- SELECT entity_id, type_id, data, created_at, updated_at, deleted_at
65
- FROM components
66
- WHERE entity_id IN ${entityIdList.sql}
67
- AND type_id IN ${typeIdList.sql}
68
- AND deleted_at IS NULL
69
- `, [...entityIdList.params, ...typeIdList.params]);
70
- const map = new Map<string, ComponentData>();
71
- rows.forEach((row: any) => {
72
- const key = `${row.entity_id}-${row.type_id}`;
73
- map.set(key, {
94
+ // Filter out keys with empty/invalid entity IDs to prevent PostgreSQL UUID parsing errors
95
+ const validKeys = keys.filter(k => k.entityId && typeof k.entityId === 'string' && k.entityId.trim() !== '');
96
+ if (validKeys.length === 0) {
97
+ return keys.map(() => null);
98
+ }
99
+
100
+ const results = new Map<string, ComponentData | null>();
101
+
102
+ // Check cache first if cache manager is available
103
+ let cacheHits = 0;
104
+ let cacheMisses = 0;
105
+ if (cacheManager && cacheManager.getConfig().enabled && cacheManager.getConfig().component?.enabled) {
106
+ try {
107
+ const cachedComponents = await cacheManager.getComponents(validKeys);
108
+ cachedComponents.forEach((component, index) => {
109
+ if (component) {
110
+ const key = `${validKeys[index]!.entityId}-${validKeys[index]!.typeId}`;
111
+ results.set(key, component);
112
+ cacheHits++;
113
+ } else {
114
+ cacheMisses++;
115
+ }
116
+ });
117
+ } catch (error: any) {
118
+ logger.warn({ scope: 'cache', component: 'RequestLoaders', msg: 'Cache read failed for components, falling back to database', error });
119
+ cacheMisses += validKeys.length;
120
+ }
121
+ } else {
122
+ cacheMisses += validKeys.length;
123
+ }
124
+
125
+ // Log cache hit/miss rates for monitoring
126
+ if (validKeys.length > 0) {
127
+ const hitRate = (cacheHits / validKeys.length) * 100;
128
+ logger.debug({
129
+ scope: 'cache',
130
+ component: 'RequestLoaders',
131
+ msg: 'Component cache statistics',
132
+ total: validKeys.length,
133
+ hits: cacheHits,
134
+ misses: cacheMisses,
135
+ hitRate: `${hitRate.toFixed(1)}%`
136
+ });
137
+ }
138
+
139
+ // Find missing components that weren't in cache
140
+ const missingKeys = validKeys.filter(k => !results.has(`${k.entityId}-${k.typeId}`));
141
+
142
+ if (missingKeys.length > 0) {
143
+ const entityIds = [...new Set(missingKeys.map(k => k.entityId))];
144
+ const typeIds = [...new Set(missingKeys.map(k => k.typeId))];
145
+ const entityIdList = inList(entityIds, 1);
146
+ const typeIdList = inList(typeIds, entityIdList.newParamIndex);
147
+ const rows = await db.unsafe(`
148
+ SELECT id, entity_id, type_id, data, created_at, updated_at, deleted_at
149
+ FROM components
150
+ WHERE entity_id IN ${entityIdList.sql}
151
+ AND type_id IN ${typeIdList.sql}
152
+ AND deleted_at IS NULL
153
+ `, [...entityIdList.params, ...typeIdList.params]);
154
+
155
+ const components: ComponentData[] = rows.map((row: any) => ({
156
+ id: row.id,
157
+ entityId: row.entity_id,
74
158
  typeId: row.type_id,
75
159
  data: row.data,
76
160
  createdAt: row.created_at,
77
161
  updatedAt: row.updated_at,
78
162
  deletedAt: row.deleted_at,
163
+ }));
164
+
165
+ // Cache the loaded components if cache is enabled
166
+ if (cacheManager && cacheManager.getConfig().enabled && cacheManager.getConfig().component?.enabled) {
167
+ try {
168
+ await cacheManager.setComponentsWriteThrough(components, cacheManager.getConfig().component!.ttl);
169
+ } catch (error: any) {
170
+ logger.warn({ scope: 'cache', component: 'RequestLoaders', msg: 'Cache write failed for components', error });
171
+ }
172
+ }
173
+
174
+ components.forEach((comp: ComponentData) => {
175
+ const key = `${comp.entityId}-${comp.typeId}`;
176
+ results.set(key, comp);
79
177
  });
80
- });
81
-
178
+ }
179
+
82
180
  const duration = Date.now() - startTime;
83
181
  if (duration > 1000) { // Log slow queries
84
- console.warn(`Slow componentsByEntityType query: ${duration}ms for ${keys.length} keys`);
182
+ logger.warn(`Slow componentsByEntityType query: ${duration}ms for ${keys.length} keys`);
85
183
  }
86
184
 
87
- return keys.map(k => map.get(`${k.entityId}-${k.typeId}`) ?? null);
88
- } catch (error) {
89
- console.error(`Error in componentsByEntityType DataLoader:`, error);
185
+ // Return null for keys with invalid entity IDs
186
+ return keys.map(k => {
187
+ if (!k.entityId || typeof k.entityId !== 'string' || k.entityId.trim() === '') return null;
188
+ return results.get(`${k.entityId}-${k.typeId}`) ?? null;
189
+ });
190
+ } catch (error: any) {
191
+ logger.error(`Error in componentsByEntityType DataLoader:`, error);
90
192
  throw error;
91
193
  }
194
+ },
195
+ {
196
+ maxBatchSize: 100 // Prevent extremely large batches
92
197
  }
93
198
  );
94
199
 
@@ -96,73 +201,98 @@ export function createRequestLoaders(db: any): RequestLoaders {
96
201
  async (keys: readonly { entityId: string; relationField: string; relatedType: string; foreignKey?: string }[]) => {
97
202
  const startTime = Date.now();
98
203
  try {
99
- // Group keys by relation type for efficient querying
204
+ // Filter valid keys
205
+ const validKeys = keys.filter(k => k.entityId && typeof k.entityId === 'string' && k.entityId.trim() !== '');
206
+ if (validKeys.length === 0) {
207
+ return keys.map(() => []);
208
+ }
209
+
210
+ // Group keys by foreign key for efficient batching
211
+ const keysByForeignKey = new Map<string, typeof validKeys>();
212
+ for (const key of validKeys) {
213
+ const fk = key.foreignKey || 'default';
214
+ if (!keysByForeignKey.has(fk)) {
215
+ keysByForeignKey.set(fk, []);
216
+ }
217
+ keysByForeignKey.get(fk)!.push(key);
218
+ }
219
+
100
220
  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[] = [];
221
+
222
+ // OPTIMIZED: Batch query for each foreign key type (instead of N separate queries)
223
+ for (const [foreignKey, groupedKeys] of keysByForeignKey) {
224
+ const entityIds = [...new Set(groupedKeys.map(k => k.entityId))];
225
+ const entityIdList = inList(entityIds, 1);
226
+
227
+ let foreignKeyField: string;
228
+ let whereClause: string;
105
229
 
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);
230
+ if (foreignKey !== 'default') {
231
+ // Use specific foreign key from relation metadata
232
+ foreignKeyField = foreignKey;
233
+ whereClause = `c.data->>'${foreignKey}' = ANY($1)`;
234
+ } else {
235
+ // Fallback for backward compatibility
236
+ foreignKeyField = 'user_id'; // Default field for result mapping
237
+ whereClause = `(c.data->>'user_id' = ANY($1) OR c.data->>'parent_id' = ANY($1))`;
238
+ }
239
+
240
+ logger.trace(`[RelationLoader] Batched query for ${groupedKeys.length} keys with foreign key ${foreignKey}`);
241
+
242
+ // SINGLE BATCHED QUERY for all entities in this group
243
+ const rows = await db.unsafe(`
244
+ SELECT DISTINCT
245
+ c.entity_id,
246
+ c.data,
247
+ c.type_id,
248
+ c.data->>'${foreignKeyField}' as fk_value,
249
+ COALESCE(c.data->>'user_id', c.data->>'parent_id') as fallback_fk_value
250
+ FROM components c
251
+ INNER JOIN entities e ON c.entity_id = e.id
252
+ WHERE e.deleted_at IS NULL
253
+ AND c.deleted_at IS NULL
254
+ AND ${whereClause}
255
+ `, [entityIds]);
256
+
257
+ logger.trace(`[RelationLoader] Found ${rows.length} total components for ${entityIds.length} entities`);
258
+
259
+ // Map results back to original keys
260
+ for (const key of groupedKeys) {
261
+ const relatedEntityIds = rows
262
+ .filter((row: any) => {
263
+ // Match by specific foreign key or fallback
264
+ const fkValue = foreignKey !== 'default' ? row.fk_value : row.fallback_fk_value;
265
+ return fkValue === key.entityId;
266
+ })
267
+ .map((row: any) => row.entity_id);
268
+
269
+ const uniqueEntityIds = [...new Set(relatedEntityIds)];
270
+ const entities = uniqueEntityIds.map(id => {
271
+ const entity = new Entity(id as string);
141
272
  entity.setPersisted(true);
142
273
  return entity;
143
274
  });
144
275
 
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 = [];
276
+ const mapKey = `${key.entityId}-${key.relationField}-${key.relatedType}`;
277
+ resultMap.set(mapKey, entities);
278
+
279
+ logger.trace(`[RelationLoader] Mapped ${entities.length} entities for ${key.relationField} on ${key.entityId}`);
151
280
  }
152
-
153
- const mapKey = `${key.entityId}-${key.relationField}-${key.relatedType}`;
154
- resultMap.set(mapKey, relatedEntities);
155
281
  }
156
-
282
+
157
283
  const duration = Date.now() - startTime;
158
284
  if (duration > 1000) {
159
285
  logger.warn(`Slow relationsByEntityField query: ${duration}ms for ${keys.length} keys`);
286
+ } else {
287
+ logger.trace(`[RelationLoader] Batched query completed in ${duration}ms for ${keys.length} keys`);
160
288
  }
161
-
289
+
162
290
  return keys.map(k => {
291
+ if (!k.entityId || typeof k.entityId !== 'string' || k.entityId.trim() === '') {
292
+ return [];
293
+ }
163
294
  const mapKey = `${k.entityId}-${k.relationField}-${k.relatedType}`;
164
295
  const result = resultMap.get(mapKey) || [];
165
- logger.trace(`[RelationLoader] Returning ${result.length} entities for ${k.relationField} on ${k.entityId}`);
166
296
  return result;
167
297
  });
168
298
  } catch (error) {
@@ -171,6 +301,10 @@ export function createRequestLoaders(db: any): RequestLoaders {
171
301
  // Return empty arrays for all keys on error
172
302
  return keys.map(() => []);
173
303
  }
304
+ },
305
+ {
306
+ // Add batch size limit to prevent extremely large queries
307
+ maxBatchSize: 50
174
308
  }
175
309
  );
176
310