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.
- package/.claude/settings.local.json +47 -0
- package/.claude/skills/update-memory.md +74 -0
- package/.prettierrc +4 -0
- package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
- package/.serena/memories/architecture.md +154 -0
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
- package/.serena/memories/code_style_and_conventions.md +76 -0
- package/.serena/memories/project_overview.md +43 -0
- package/.serena/memories/schema-dsl-plan.md +107 -0
- package/.serena/memories/suggested_commands.md +80 -0
- package/.serena/memories/typescript-compilation-status.md +54 -0
- package/.serena/project.yml +114 -0
- package/TODO.md +1 -7
- package/bun.lock +150 -4
- package/bunfig.toml +10 -0
- package/config/cache.config.ts +77 -0
- package/config/upload.config.ts +4 -5
- package/core/App.ts +870 -123
- package/core/ArcheType.ts +2268 -377
- package/core/BatchLoader.ts +181 -71
- package/core/Config.ts +153 -0
- package/core/Decorators.ts +4 -1
- package/core/Entity.ts +621 -92
- package/core/EntityHookManager.ts +1 -1
- package/core/EntityInterface.ts +3 -1
- package/core/EntityManager.ts +1 -13
- package/core/ErrorHandler.ts +8 -2
- package/core/Logger.ts +9 -0
- package/core/Middleware.ts +34 -0
- package/core/RequestContext.ts +5 -1
- package/core/RequestLoaders.ts +227 -93
- package/core/SchedulerManager.ts +193 -52
- package/core/cache/CacheAnalytics.ts +399 -0
- package/core/cache/CacheFactory.ts +145 -0
- package/core/cache/CacheManager.ts +520 -0
- package/core/cache/CacheProvider.ts +34 -0
- package/core/cache/CacheWarmer.ts +157 -0
- package/core/cache/CompressionUtils.ts +110 -0
- package/core/cache/MemoryCache.ts +251 -0
- package/core/cache/MultiLevelCache.ts +180 -0
- package/core/cache/NoOpCache.ts +53 -0
- package/core/cache/RedisCache.ts +464 -0
- package/core/cache/TTLStrategy.ts +254 -0
- package/core/cache/index.ts +6 -0
- package/core/components/BaseComponent.ts +120 -0
- package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
- package/core/components/Decorators.ts +88 -0
- package/core/components/Interfaces.ts +7 -0
- package/core/components/index.ts +5 -0
- package/core/decorators/EntityHooks.ts +0 -3
- package/core/decorators/IndexedField.ts +26 -0
- package/core/decorators/ScheduledTask.ts +0 -47
- package/core/events/EntityLifecycleEvents.ts +1 -1
- package/core/health.ts +112 -0
- package/core/metadata/definitions/ArcheType.ts +14 -0
- package/core/metadata/definitions/Component.ts +9 -0
- package/core/metadata/definitions/gqlObject.ts +1 -1
- package/core/metadata/index.ts +42 -1
- package/core/metadata/metadata-storage.ts +28 -2
- package/core/middleware/AccessLog.ts +59 -0
- package/core/middleware/RequestId.ts +38 -0
- package/core/middleware/SecurityHeaders.ts +62 -0
- package/core/middleware/index.ts +3 -0
- package/core/scheduler/DistributedLock.ts +266 -0
- package/core/scheduler/index.ts +15 -0
- package/core/validateEnv.ts +92 -0
- package/database/DatabaseHelper.ts +416 -40
- package/database/IndexingStrategy.ts +342 -0
- package/database/PreparedStatementCache.ts +226 -0
- package/database/index.ts +32 -7
- package/database/sqlHelpers.ts +14 -2
- package/endpoints/archetypes.ts +362 -0
- package/endpoints/components.ts +58 -0
- package/endpoints/entity.ts +80 -0
- package/endpoints/index.ts +27 -0
- package/endpoints/query.ts +93 -0
- package/endpoints/stats.ts +76 -0
- package/endpoints/tables.ts +212 -0
- package/endpoints/types.ts +155 -0
- package/gql/ArchetypeOperations.ts +32 -86
- package/gql/Generator.ts +27 -315
- package/gql/GeneratorV2.ts +37 -0
- package/gql/builders/InputTypeBuilder.ts +99 -0
- package/gql/builders/ResolverBuilder.ts +234 -0
- package/gql/builders/TypeDefBuilder.ts +105 -0
- package/gql/builders/index.ts +3 -0
- package/gql/decorators/Upload.ts +1 -1
- package/gql/depthLimit.ts +85 -0
- package/gql/graph/GraphNode.ts +224 -0
- package/gql/graph/SchemaGraph.ts +278 -0
- package/gql/helpers.ts +8 -2
- package/gql/index.ts +56 -4
- package/gql/middleware.ts +79 -0
- package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
- package/gql/orchestration/index.ts +1 -0
- package/gql/scanner/ServiceScanner.ts +347 -0
- package/gql/schema/index.ts +458 -0
- package/gql/strategies/TypeGenerationStrategy.ts +329 -0
- package/gql/types.ts +1 -0
- package/gql/utils/TypeSignature.ts +220 -0
- package/gql/utils/index.ts +1 -0
- package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
- package/gql/visitors/DeduplicationVisitor.ts +82 -0
- package/gql/visitors/GraphVisitor.ts +78 -0
- package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
- package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
- package/gql/visitors/TypeCollectorVisitor.ts +79 -0
- package/gql/visitors/VisitorComposer.ts +96 -0
- package/gql/visitors/index.ts +7 -0
- package/package.json +59 -37
- package/plugins/index.ts +2 -2
- package/query/CTENode.ts +97 -0
- package/query/ComponentInclusionNode.ts +689 -0
- package/query/FilterBuilder.ts +127 -0
- package/query/FilterBuilderRegistry.ts +202 -0
- package/query/OrNode.ts +517 -0
- package/query/OrQuery.ts +42 -0
- package/query/Query.ts +1022 -0
- package/query/QueryContext.ts +170 -0
- package/query/QueryDAG.ts +122 -0
- package/query/QueryNode.ts +65 -0
- package/query/SourceNode.ts +53 -0
- package/query/builders/FullTextSearchBuilder.ts +236 -0
- package/query/index.ts +21 -0
- package/scheduler/index.ts +40 -8
- package/service/Service.ts +2 -1
- package/service/ServiceRegistry.ts +6 -5
- package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
- package/storage/S3StorageProvider.ts +316 -0
- package/{core/storage → storage}/StorageProvider.ts +7 -3
- package/studio/bun.lock +482 -0
- package/studio/index.html +13 -0
- package/studio/package.json +39 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/components/DataTable.tsx +211 -0
- package/studio/src/components/Layout.tsx +13 -0
- package/studio/src/components/PageContainer.tsx +9 -0
- package/studio/src/components/PageHeader.tsx +13 -0
- package/studio/src/components/SearchBar.tsx +57 -0
- package/studio/src/components/Sidebar.tsx +294 -0
- package/studio/src/components/ui/button.tsx +56 -0
- package/studio/src/components/ui/checkbox.tsx +26 -0
- package/studio/src/components/ui/input.tsx +25 -0
- package/studio/src/hooks/useDataTable.ts +131 -0
- package/studio/src/index.css +36 -0
- package/studio/src/lib/api.ts +186 -0
- package/studio/src/lib/utils.ts +13 -0
- package/studio/src/main.tsx +17 -0
- package/studio/src/pages/ArcheType.tsx +239 -0
- package/studio/src/pages/Components.tsx +124 -0
- package/studio/src/pages/EntityInspector.tsx +302 -0
- package/studio/src/pages/QueryRunner.tsx +246 -0
- package/studio/src/pages/Table.tsx +94 -0
- package/studio/src/pages/Welcome.tsx +241 -0
- package/studio/src/routes.tsx +45 -0
- package/studio/src/store/archeTypeSettings.ts +30 -0
- package/studio/src/store/studio.ts +65 -0
- package/studio/src/utils/columnHelpers.tsx +114 -0
- package/studio/studio-instructions.md +81 -0
- package/studio/tailwind.config.js +77 -0
- package/studio/tsconfig.json +24 -0
- package/studio/utils.ts +54 -0
- package/studio/vite.config.js +19 -0
- package/swagger/generator.ts +1 -1
- package/tests/e2e/http.test.ts +126 -0
- package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
- package/tests/fixtures/components/TestOrder.ts +23 -0
- package/tests/fixtures/components/TestProduct.ts +23 -0
- package/tests/fixtures/components/TestUser.ts +20 -0
- package/tests/fixtures/components/index.ts +6 -0
- package/tests/graphql/SchemaGeneration.test.ts +90 -0
- package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
- package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
- package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
- package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
- package/tests/integration/entity/Entity.persistence.test.ts +333 -0
- package/tests/integration/query/Query.exec.test.ts +523 -0
- package/tests/pglite-setup.ts +61 -0
- package/tests/setup.ts +164 -0
- package/tests/stress/BenchmarkRunner.ts +203 -0
- package/tests/stress/DataSeeder.ts +190 -0
- package/tests/stress/StressTestReporter.ts +229 -0
- package/tests/stress/cursor-perf-test.ts +171 -0
- package/tests/stress/fixtures/StressTestComponents.ts +58 -0
- package/tests/stress/index.ts +7 -0
- package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
- package/tests/unit/BatchLoader.test.ts +82 -0
- package/tests/unit/archetype/ArcheType.test.ts +107 -0
- package/tests/unit/cache/CacheManager.test.ts +347 -0
- package/tests/unit/cache/MemoryCache.test.ts +260 -0
- package/tests/unit/cache/RedisCache.test.ts +411 -0
- package/tests/unit/entity/Entity.components.test.ts +244 -0
- package/tests/unit/entity/Entity.test.ts +345 -0
- package/tests/unit/gql/depthLimit.test.ts +203 -0
- package/tests/unit/gql/operationMiddleware.test.ts +293 -0
- package/tests/unit/health/Health.test.ts +129 -0
- package/tests/unit/middleware/AccessLog.test.ts +37 -0
- package/tests/unit/middleware/Middleware.test.ts +98 -0
- package/tests/unit/middleware/RequestId.test.ts +54 -0
- package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
- package/tests/unit/query/FilterBuilder.test.ts +111 -0
- package/tests/unit/query/Query.test.ts +308 -0
- package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
- package/tests/unit/schema/schema-integration.test.ts +426 -0
- package/tests/unit/schema/schema.test.ts +580 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
- package/tests/unit/upload/RestUpload.test.ts +267 -0
- package/tests/unit/validateEnv.test.ts +82 -0
- package/tests/utils/entity-tracker.ts +57 -0
- package/tests/utils/index.ts +13 -0
- package/tests/utils/test-context.ts +149 -0
- package/tsconfig.json +5 -1
- package/types/archetype.types.ts +6 -0
- package/types/hooks.types.ts +1 -1
- package/types/query.types.ts +110 -0
- package/types/scheduler.types.ts +68 -7
- package/types/upload.types.ts +1 -0
- package/{core → upload}/FileValidator.ts +10 -1
- package/upload/RestUpload.ts +130 -0
- package/{core/components → upload}/UploadComponent.ts +11 -11
- package/{core → upload}/UploadManager.ts +3 -3
- package/upload/index.ts +23 -7
- package/utils/UploadHelper.ts +27 -6
- package/utils/cronParser.ts +16 -6
- package/.github/workflows/deploy-docs.yml +0 -57
- package/core/Components.ts +0 -202
- package/core/EntityCache.ts +0 -15
- package/core/Query.ts +0 -880
- package/docs/README.md +0 -149
- package/docs/_coverpage.md +0 -36
- package/docs/_sidebar.md +0 -23
- package/docs/api/core.md +0 -568
- package/docs/api/hooks.md +0 -554
- package/docs/api/index.md +0 -222
- package/docs/api/query.md +0 -678
- package/docs/api/service.md +0 -744
- package/docs/core-concepts/archetypes.md +0 -512
- package/docs/core-concepts/components.md +0 -498
- package/docs/core-concepts/entity.md +0 -314
- package/docs/core-concepts/hooks.md +0 -683
- package/docs/core-concepts/query.md +0 -588
- package/docs/core-concepts/services.md +0 -647
- package/docs/examples/code-examples.md +0 -425
- package/docs/getting-started.md +0 -337
- package/docs/index.html +0 -97
- package/tests/bench/insert.bench.ts +0 -60
- package/tests/bench/relations.bench.ts +0 -270
- package/tests/bench/sorting.bench.ts +0 -416
- package/tests/component-hooks-simple.test.ts +0 -117
- package/tests/component-hooks.test.ts +0 -1461
- package/tests/component.test.ts +0 -339
- package/tests/errorHandling.test.ts +0 -155
- package/tests/hooks.test.ts +0 -667
- package/tests/query-sorting.test.ts +0 -101
- package/tests/query.test.ts +0 -81
- package/tests/relations.test.ts +0 -170
- package/tests/scheduler.test.ts +0 -724
package/core/EntityInterface.ts
CHANGED
package/core/EntityManager.ts
CHANGED
|
@@ -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.
|
|
31
|
+
promiseWait.push(entity.save());
|
|
44
32
|
}
|
|
45
33
|
return await Promise.all(promiseWait);
|
|
46
34
|
}
|
package/core/ErrorHandler.ts
CHANGED
|
@@ -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
|
+
}
|
package/core/RequestContext.ts
CHANGED
|
@@ -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
|
};
|
package/core/RequestLoaders.ts
CHANGED
|
@@ -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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
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
|
-
|
|
74
|
+
logger.warn(`Slow entityById query: ${duration}ms for ${ids.length} entities`);
|
|
46
75
|
}
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
182
|
+
logger.warn(`Slow componentsByEntityType query: ${duration}ms for ${keys.length} keys`);
|
|
85
183
|
}
|
|
86
184
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
103
|
-
for (const
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
logger.
|
|
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
|
|