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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GraphVisitor } from "./GraphVisitor";
|
|
2
|
+
import { TypeNode, OperationNode, FieldNode, InputNode, ScalarNode } from "../graph/GraphNode";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Visitor that collects all type definitions in dependency order.
|
|
6
|
+
* Used to gather type definitions before schema generation.
|
|
7
|
+
*/
|
|
8
|
+
export class TypeCollectorVisitor extends GraphVisitor {
|
|
9
|
+
private collectedTypes: Map<string, string> = new Map();
|
|
10
|
+
private collectedInputs: Map<string, string> = new Map();
|
|
11
|
+
private collectedScalars: Set<string> = new Set();
|
|
12
|
+
|
|
13
|
+
visitTypeNode(node: TypeNode): void {
|
|
14
|
+
this.collectedTypes.set(node.name, node.typeDef);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
visitOperationNode(node: OperationNode): void {
|
|
18
|
+
// Operations don't define types themselves, but may reference them
|
|
19
|
+
// This visitor focuses on collecting type definitions
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
visitFieldNode(node: FieldNode): void {
|
|
23
|
+
// Fields don't define types themselves
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
visitInputNode(node: InputNode): void {
|
|
27
|
+
this.collectedInputs.set(node.name, node.typeDef);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
visitScalarNode(node: ScalarNode): void {
|
|
31
|
+
this.collectedScalars.add(node.name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getResults(): {
|
|
35
|
+
types: Map<string, string>;
|
|
36
|
+
inputs: Map<string, string>;
|
|
37
|
+
scalars: Set<string>;
|
|
38
|
+
} {
|
|
39
|
+
return {
|
|
40
|
+
types: new Map(this.collectedTypes),
|
|
41
|
+
inputs: new Map(this.collectedInputs),
|
|
42
|
+
scalars: new Set(this.collectedScalars)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get all type definitions as a single string in dependency order
|
|
48
|
+
*/
|
|
49
|
+
getTypeDefsString(): string {
|
|
50
|
+
const results = this.getResults();
|
|
51
|
+
let typeDefs = '';
|
|
52
|
+
|
|
53
|
+
// Add scalar definitions first
|
|
54
|
+
for (const scalarName of results.scalars) {
|
|
55
|
+
typeDefs += `scalar ${scalarName}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add input types
|
|
59
|
+
for (const [name, typeDef] of results.inputs) {
|
|
60
|
+
typeDefs += `${typeDef}\n`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add object types
|
|
64
|
+
for (const [name, typeDef] of results.types) {
|
|
65
|
+
typeDefs += `${typeDef}\n`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return typeDefs.trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clear all collected types
|
|
73
|
+
*/
|
|
74
|
+
clear(): void {
|
|
75
|
+
this.collectedTypes.clear();
|
|
76
|
+
this.collectedInputs.clear();
|
|
77
|
+
this.collectedScalars.clear();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { GraphVisitor } from "./GraphVisitor";
|
|
2
|
+
import { SchemaGraph } from "../graph/SchemaGraph";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composes multiple visitors to run them in sequence on a graph.
|
|
6
|
+
* Allows chaining visitor operations for complex processing pipelines.
|
|
7
|
+
*/
|
|
8
|
+
export class VisitorComposer {
|
|
9
|
+
private visitors: GraphVisitor[] = [];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Add a visitor to the composition
|
|
13
|
+
*/
|
|
14
|
+
addVisitor(visitor: GraphVisitor): VisitorComposer {
|
|
15
|
+
this.visitors.push(visitor);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run all visitors on the given graph in sequence
|
|
21
|
+
*/
|
|
22
|
+
visitGraph(graph: SchemaGraph): Record<string, any> {
|
|
23
|
+
for (const visitor of this.visitors) {
|
|
24
|
+
// Call beforeVisit for setup
|
|
25
|
+
visitor.beforeVisit();
|
|
26
|
+
|
|
27
|
+
// Visit all nodes in topological order
|
|
28
|
+
const sortedNodes = graph.topologicalSort();
|
|
29
|
+
for (const node of sortedNodes) {
|
|
30
|
+
visitor.visit(node);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Call afterVisit for cleanup
|
|
34
|
+
visitor.afterVisit();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return this.getResults();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run all visitors on a specific set of nodes
|
|
42
|
+
*/
|
|
43
|
+
visitNodes(nodes: any[]): Record<string, any> {
|
|
44
|
+
for (const visitor of this.visitors) {
|
|
45
|
+
for (const node of nodes) {
|
|
46
|
+
visitor.visit(node);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return this.getResults();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remove a specific visitor from the composition
|
|
55
|
+
*/
|
|
56
|
+
removeVisitor(visitor: GraphVisitor): boolean {
|
|
57
|
+
const index = this.visitors.indexOf(visitor);
|
|
58
|
+
if (index > -1) {
|
|
59
|
+
this.visitors.splice(index, 1);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Clear all visitors from the composer
|
|
67
|
+
*/
|
|
68
|
+
clearVisitors(): void {
|
|
69
|
+
this.visitors = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get results from all visitors as an object
|
|
74
|
+
*/
|
|
75
|
+
getResults(): Record<string, any> {
|
|
76
|
+
const results: Record<string, any> = {};
|
|
77
|
+
this.visitors.forEach((visitor, index) => {
|
|
78
|
+
results[`visitor-${index}`] = visitor.getResults();
|
|
79
|
+
});
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the number of visitors in the composition
|
|
85
|
+
*/
|
|
86
|
+
size(): number {
|
|
87
|
+
return this.visitors.length;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get all visitors in the composition
|
|
92
|
+
*/
|
|
93
|
+
getVisitors(): GraphVisitor[] {
|
|
94
|
+
return [...this.visitors];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { GraphVisitor } from "./GraphVisitor";
|
|
2
|
+
export { TypeCollectorVisitor } from "./TypeCollectorVisitor";
|
|
3
|
+
export { DeduplicationVisitor } from "./DeduplicationVisitor";
|
|
4
|
+
export { SchemaGeneratorVisitor } from "./SchemaGeneratorVisitor";
|
|
5
|
+
export { ResolverGeneratorVisitor } from "./ResolverGeneratorVisitor";
|
|
6
|
+
export { ArchetypePreprocessorVisitor } from "./ArchetypePreprocessorVisitor";
|
|
7
|
+
export { VisitorComposer } from "./VisitorComposer";
|
package/package.json
CHANGED
|
@@ -1,39 +1,61 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
2
|
+
"name": "bunsane",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "yaaruu"
|
|
6
|
+
},
|
|
7
|
+
"keywords": [
|
|
8
|
+
"bun",
|
|
9
|
+
"framework",
|
|
10
|
+
"entity-component-system",
|
|
11
|
+
"ecs",
|
|
12
|
+
"graphql",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"module": "index.ts",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun run build:studio && tsc",
|
|
19
|
+
"build:studio": "cd studio && bun install && bun run build",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"test": "bun test tests/unit tests/integration tests/graphql",
|
|
22
|
+
"test:all": "bun test tests/",
|
|
23
|
+
"test:unit": "bun test tests/unit/",
|
|
24
|
+
"test:integration": "bun test tests/integration/",
|
|
25
|
+
"test:graphql": "bun test tests/graphql/",
|
|
26
|
+
"test:pglite": "bun tests/pglite-setup.ts tests/unit tests/integration tests/graphql",
|
|
27
|
+
"test:pglite:unit": "bun tests/pglite-setup.ts tests/unit/",
|
|
28
|
+
"test:e2e": "SKIP_TEST_DB_SETUP=true bun test tests/e2e/",
|
|
29
|
+
"test:stress": "bun test tests/stress --timeout 600000",
|
|
30
|
+
"test:stress:smoke": "STRESS_RECORD_COUNT=10000 bun test tests/stress --timeout 300000",
|
|
31
|
+
"test:stress:standard": "STRESS_RECORD_COUNT=100000 bun test tests/stress --timeout 600000",
|
|
32
|
+
"test:stress:full": "STRESS_RECORD_COUNT=1000000 bun test tests/stress --timeout 1800000"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@electric-sql/pglite": "^0.3.15",
|
|
36
|
+
"@electric-sql/pglite-socket": "^0.0.20",
|
|
37
|
+
"@types/bun": "latest",
|
|
38
|
+
"@types/node": "^25.0.3",
|
|
39
|
+
"knip": "^5.78.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@gqloom/core": "^0.12.0",
|
|
46
|
+
"@gqloom/zod": "^0.12.2",
|
|
47
|
+
"dataloader": "2.2.2",
|
|
48
|
+
"graphql": "16.11.0",
|
|
49
|
+
"graphql-yoga": "5.15.1",
|
|
50
|
+
"ioredis": "5.8.2",
|
|
51
|
+
"pino": "9.9.0",
|
|
52
|
+
"pino-pretty": "13.1.1",
|
|
53
|
+
"reflect-metadata": "0.2.2",
|
|
54
|
+
"zod": "4.1.5"
|
|
55
|
+
},
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "https://github.com/yaaruu/bunsane.git"
|
|
59
|
+
},
|
|
60
|
+
"license": "MIT"
|
|
39
61
|
}
|
package/plugins/index.ts
CHANGED
package/query/CTENode.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { QueryNode } from "./QueryNode";
|
|
2
|
+
import type { QueryResult } from "./QueryNode";
|
|
3
|
+
import { QueryContext } from "./QueryContext";
|
|
4
|
+
|
|
5
|
+
export class CTENode extends QueryNode {
|
|
6
|
+
public execute(context: QueryContext): QueryResult {
|
|
7
|
+
// Generate CTE for base entity filtering
|
|
8
|
+
const componentIds = Array.from(context.componentIds);
|
|
9
|
+
const excludedIds = Array.from(context.excludedComponentIds);
|
|
10
|
+
|
|
11
|
+
if (componentIds.length === 0) {
|
|
12
|
+
throw new Error("CTENode requires at least one component type to filter on");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let cteSql = "WITH base_entities AS (\n";
|
|
16
|
+
cteSql += " SELECT DISTINCT ec.entity_id\n";
|
|
17
|
+
cteSql += " FROM entity_components ec\n";
|
|
18
|
+
cteSql += " WHERE ec.type_id IN (";
|
|
19
|
+
|
|
20
|
+
// Add component type placeholders
|
|
21
|
+
const typePlaceholders = componentIds.map((_, index) => `$${context.addParam(componentIds[index])}`).join(', ');
|
|
22
|
+
cteSql += typePlaceholders + ")\n";
|
|
23
|
+
cteSql += " AND ec.deleted_at IS NULL\n";
|
|
24
|
+
|
|
25
|
+
// Add cursor-based pagination filter in CTE (more efficient than OFFSET)
|
|
26
|
+
if (context.cursorId !== null) {
|
|
27
|
+
const operator = context.cursorDirection === 'after' ? '>' : '<';
|
|
28
|
+
cteSql += ` AND ec.entity_id ${operator} $${context.addParam(context.cursorId)}\n`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Add exclusions if any
|
|
32
|
+
if (excludedIds.length > 0) {
|
|
33
|
+
const excludedPlaceholders = excludedIds.map((id) => `$${context.addParam(id)}`).join(', ');
|
|
34
|
+
cteSql += ` AND NOT EXISTS (\n`;
|
|
35
|
+
cteSql += ` SELECT 1 FROM entity_components ec_ex\n`;
|
|
36
|
+
cteSql += ` WHERE ec_ex.entity_id = ec.entity_id\n`;
|
|
37
|
+
cteSql += ` AND ec_ex.type_id IN (${excludedPlaceholders})\n`;
|
|
38
|
+
cteSql += ` AND ec_ex.deleted_at IS NULL\n`;
|
|
39
|
+
cteSql += ` )\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add entity exclusions if any
|
|
43
|
+
if (context.excludedEntityIds.size > 0) {
|
|
44
|
+
const entityExcludedIds = Array.from(context.excludedEntityIds);
|
|
45
|
+
const entityPlaceholders = entityExcludedIds.map((id) => `$${context.addParam(id)}`).join(', ');
|
|
46
|
+
cteSql += ` AND ec.entity_id NOT IN (${entityPlaceholders})\n`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Group by entity_id to count distinct component types
|
|
50
|
+
// This ensures entities have ALL required components
|
|
51
|
+
cteSql += ` GROUP BY ec.entity_id\n`;
|
|
52
|
+
cteSql += ` HAVING COUNT(DISTINCT ec.type_id) >= $${context.addParam(componentIds.length)}\n`;
|
|
53
|
+
|
|
54
|
+
// Add ORDER BY for deterministic pagination results
|
|
55
|
+
// Must be before LIMIT/OFFSET for consistent page results
|
|
56
|
+
// Reverse order for 'before' cursor direction
|
|
57
|
+
const orderDirection = context.cursorDirection === 'before' ? 'DESC' : 'ASC';
|
|
58
|
+
cteSql += ` ORDER BY ec.entity_id ${orderDirection}\n`;
|
|
59
|
+
|
|
60
|
+
// Check if there are component filters - if so, pagination must happen AFTER filtering
|
|
61
|
+
// Otherwise we'd limit results before applying filters, causing incorrect results
|
|
62
|
+
const hasComponentFilters = context.componentFilters.size > 0;
|
|
63
|
+
|
|
64
|
+
// Add LIMIT/OFFSET at CTE level ONLY when there are no component filters
|
|
65
|
+
// When filters exist, pagination must be applied after LATERAL joins filter the results
|
|
66
|
+
if (!hasComponentFilters) {
|
|
67
|
+
if (context.limit !== null) {
|
|
68
|
+
cteSql += ` LIMIT $${context.addParam(context.limit)}\n`;
|
|
69
|
+
}
|
|
70
|
+
// Only include OFFSET when not using cursor-based pagination
|
|
71
|
+
if (context.cursorId === null && (context.offsetValue > 0 || context.limit !== null)) {
|
|
72
|
+
cteSql += ` OFFSET $${context.addParam(context.offsetValue)}\n`;
|
|
73
|
+
}
|
|
74
|
+
// Mark pagination as handled at CTE level to prevent double application
|
|
75
|
+
context.paginationAppliedInCTE = true;
|
|
76
|
+
} else {
|
|
77
|
+
// Pagination will be applied later, after filters
|
|
78
|
+
context.paginationAppliedInCTE = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
cteSql += ")";
|
|
82
|
+
|
|
83
|
+
// Mark CTE as available in context
|
|
84
|
+
context.hasCTE = true;
|
|
85
|
+
context.cteName = "base_entities";
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
sql: cteSql,
|
|
89
|
+
params: context.params,
|
|
90
|
+
context
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public getNodeType(): string {
|
|
95
|
+
return "CTENode";
|
|
96
|
+
}
|
|
97
|
+
}
|