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,170 @@
|
|
|
1
|
+
import { ComponentRegistry, type BaseComponent, type ComponentDataType } from "../core/components";
|
|
2
|
+
import { FilterBuilderRegistry } from "./FilterBuilderRegistry";
|
|
3
|
+
import type { SQL } from "bun";
|
|
4
|
+
|
|
5
|
+
export interface QueryFilter {
|
|
6
|
+
field: string;
|
|
7
|
+
operator: string;
|
|
8
|
+
value: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SortOrder {
|
|
12
|
+
component: string;
|
|
13
|
+
property: string;
|
|
14
|
+
direction: "ASC" | "DESC";
|
|
15
|
+
nullsFirst?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class QueryContext {
|
|
19
|
+
public params: any[] = [];
|
|
20
|
+
public paramIndex: number = 1;
|
|
21
|
+
public tableAliases: Map<string, string> = new Map();
|
|
22
|
+
public sqlFragments: string[] = [];
|
|
23
|
+
public componentIds: Set<string> = new Set();
|
|
24
|
+
public excludedComponentIds: Set<string> = new Set();
|
|
25
|
+
public componentFilters: Map<string, QueryFilter[]> = new Map();
|
|
26
|
+
public sortOrders: SortOrder[] = [];
|
|
27
|
+
public excludedEntityIds: Set<string> = new Set();
|
|
28
|
+
public withId: string | null = null;
|
|
29
|
+
public limit: number | null = null;
|
|
30
|
+
public offsetValue: number = 0;
|
|
31
|
+
|
|
32
|
+
// Cursor-based pagination (more efficient than OFFSET for large datasets)
|
|
33
|
+
public cursorId: string | null = null;
|
|
34
|
+
public cursorDirection: 'after' | 'before' = 'after';
|
|
35
|
+
public hasCTE: boolean = false;
|
|
36
|
+
public cteName: string = "";
|
|
37
|
+
public eagerComponents: Set<string> = new Set();
|
|
38
|
+
public paginationAppliedInCTE: boolean = false;
|
|
39
|
+
|
|
40
|
+
private trx: SQL | undefined;
|
|
41
|
+
constructor(trx?: SQL) {
|
|
42
|
+
this.trx = trx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the database connection (transaction or default db)
|
|
47
|
+
*/
|
|
48
|
+
public getDb(): SQL | undefined {
|
|
49
|
+
return this.trx;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public getNextAlias(prefix: string = "t"): string {
|
|
53
|
+
const count = this.tableAliases.size;
|
|
54
|
+
const alias = `${prefix}${count}`;
|
|
55
|
+
this.tableAliases.set(alias, alias);
|
|
56
|
+
return alias;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public addParam(value: any): number {
|
|
60
|
+
this.params.push(value);
|
|
61
|
+
return this.paramIndex++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reset the context for reuse (clears params and resets paramIndex)
|
|
66
|
+
*/
|
|
67
|
+
public reset(): void {
|
|
68
|
+
this.params = [];
|
|
69
|
+
this.paramIndex = 1;
|
|
70
|
+
this.tableAliases.clear();
|
|
71
|
+
this.sqlFragments = [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public addParams(values: any[]): number[] {
|
|
75
|
+
const indices: number[] = [];
|
|
76
|
+
for (const value of values) {
|
|
77
|
+
indices.push(this.addParam(value));
|
|
78
|
+
}
|
|
79
|
+
return indices;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public addSqlFragment(fragment: string): void {
|
|
83
|
+
this.sqlFragments.push(fragment);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public getComponentId(componentCtor: new (...args: any[]) => BaseComponent): string | undefined {
|
|
87
|
+
return ComponentRegistry.getComponentId(componentCtor.name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate a cache key fingerprint for prepared statement caching
|
|
92
|
+
*/
|
|
93
|
+
public generateCacheKey(): string {
|
|
94
|
+
// Create a deterministic fingerprint of the query structure
|
|
95
|
+
const components = Array.from(this.componentIds).sort().join(',');
|
|
96
|
+
const excludedComponents = Array.from(this.excludedComponentIds).sort().join(',');
|
|
97
|
+
const filters = Array.from(this.componentFilters.entries())
|
|
98
|
+
.map(([typeId, filters]) => `${typeId}:${filters.map(f => {
|
|
99
|
+
// For IN/NOT IN operators, include array length in cache key
|
|
100
|
+
// This ensures different array lengths produce different cache keys
|
|
101
|
+
// preventing prepared statement parameter count mismatches
|
|
102
|
+
if ((f.operator === 'IN' || f.operator === 'NOT IN') && Array.isArray(f.value)) {
|
|
103
|
+
return `${f.field}${f.operator}[${f.value.length}]`;
|
|
104
|
+
}
|
|
105
|
+
return `${f.field}${f.operator}`;
|
|
106
|
+
}).sort().join('|')}`)
|
|
107
|
+
.sort()
|
|
108
|
+
.join(';');
|
|
109
|
+
const sorts = this.sortOrders
|
|
110
|
+
.map(s => `${s.component}.${s.property}:${s.direction}`)
|
|
111
|
+
.sort()
|
|
112
|
+
.join(',');
|
|
113
|
+
|
|
114
|
+
// Extract custom filter operators for cache key differentiation
|
|
115
|
+
const customOperators = this.extractCustomOperators();
|
|
116
|
+
const customOps = customOperators.length > 0 ? `customOps:${customOperators.sort().join(',')}` : '';
|
|
117
|
+
|
|
118
|
+
// Include pagination in cache key to prevent prepared statement collision
|
|
119
|
+
// when same query is executed with different pagination settings
|
|
120
|
+
const paginationKey = `limit:${this.limit !== null ? 'yes' : 'no'}|offset:${this.offsetValue > 0 ? 'yes' : 'no'}|cursor:${this.cursorId !== null ? this.cursorDirection : 'no'}`;
|
|
121
|
+
|
|
122
|
+
// Include excluded entity IDs count for cache key differentiation
|
|
123
|
+
const excludedEntityCount = this.excludedEntityIds.size;
|
|
124
|
+
const excludedEntitiesKey = excludedEntityCount > 0 ? `|excludedEntities:${excludedEntityCount}` : '';
|
|
125
|
+
|
|
126
|
+
const key = `${components}|${excludedComponents}|${filters}|${sorts}|${this.hasCTE}|${this.cteName}|${customOps}|${paginationKey}${excludedEntitiesKey}`;
|
|
127
|
+
return key;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract custom filter operators from component filters
|
|
132
|
+
* Used for cache key generation to differentiate queries with custom filters
|
|
133
|
+
*/
|
|
134
|
+
private extractCustomOperators(): string[] {
|
|
135
|
+
const customOperators: string[] = [];
|
|
136
|
+
|
|
137
|
+
for (const filters of this.componentFilters.values()) {
|
|
138
|
+
for (const filter of filters) {
|
|
139
|
+
if (FilterBuilderRegistry.has(filter.operator)) {
|
|
140
|
+
customOperators.push(filter.operator);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return customOperators;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public clone(): QueryContext {
|
|
149
|
+
const clone = new QueryContext();
|
|
150
|
+
clone.params = [...this.params];
|
|
151
|
+
clone.paramIndex = this.paramIndex;
|
|
152
|
+
clone.tableAliases = new Map(this.tableAliases);
|
|
153
|
+
clone.sqlFragments = [...this.sqlFragments];
|
|
154
|
+
clone.componentIds = new Set(this.componentIds);
|
|
155
|
+
clone.excludedComponentIds = new Set(this.excludedComponentIds);
|
|
156
|
+
clone.componentFilters = new Map(this.componentFilters);
|
|
157
|
+
clone.sortOrders = [...this.sortOrders];
|
|
158
|
+
clone.excludedEntityIds = new Set(this.excludedEntityIds);
|
|
159
|
+
clone.withId = this.withId;
|
|
160
|
+
clone.limit = this.limit;
|
|
161
|
+
clone.offsetValue = this.offsetValue;
|
|
162
|
+
clone.cursorId = this.cursorId;
|
|
163
|
+
clone.cursorDirection = this.cursorDirection;
|
|
164
|
+
clone.hasCTE = this.hasCTE;
|
|
165
|
+
clone.cteName = this.cteName;
|
|
166
|
+
clone.eagerComponents = new Set(this.eagerComponents);
|
|
167
|
+
clone.paginationAppliedInCTE = this.paginationAppliedInCTE;
|
|
168
|
+
return clone;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { QueryNode } from "./QueryNode";
|
|
2
|
+
import type { QueryResult } from "./QueryNode";
|
|
3
|
+
import { QueryContext } from "./QueryContext";
|
|
4
|
+
import { SourceNode } from "./SourceNode";
|
|
5
|
+
import { ComponentInclusionNode } from "./ComponentInclusionNode";
|
|
6
|
+
import { CTENode } from "./CTENode";
|
|
7
|
+
|
|
8
|
+
export class QueryDAG {
|
|
9
|
+
private nodes: QueryNode[] = [];
|
|
10
|
+
private rootNode: QueryNode | null = null;
|
|
11
|
+
|
|
12
|
+
public addNode(node: QueryNode): void {
|
|
13
|
+
if (!this.nodes.includes(node)) {
|
|
14
|
+
this.nodes.push(node);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public setRootNode(node: QueryNode): void {
|
|
19
|
+
this.rootNode = node;
|
|
20
|
+
this.addNode(node);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public getRootNode(): QueryNode | null {
|
|
24
|
+
return this.rootNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public getNodes(): QueryNode[] {
|
|
28
|
+
return [...this.nodes];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute the DAG by finding and executing the final node in the chain
|
|
33
|
+
*/
|
|
34
|
+
public execute(context: QueryContext): QueryResult {
|
|
35
|
+
if (!this.rootNode) {
|
|
36
|
+
throw new Error("No root node set in QueryDAG");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get all nodes in topological order
|
|
40
|
+
const allNodes = this.rootNode.getTopologicalOrder();
|
|
41
|
+
|
|
42
|
+
// Check if we have a CTENode and execute it first to set context
|
|
43
|
+
const cteNode = allNodes.find(node => node instanceof CTENode) as CTENode;
|
|
44
|
+
let cteSql = '';
|
|
45
|
+
if (cteNode) {
|
|
46
|
+
// Execute CTE node first to set context and get SQL
|
|
47
|
+
const cteResult = cteNode.execute(context);
|
|
48
|
+
cteSql = cteResult.sql;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The leaf node is the one that no other node depends on
|
|
52
|
+
// Find it by checking which nodes are not in any node's dependencies
|
|
53
|
+
const nodesInDependencies = new Set<QueryNode>();
|
|
54
|
+
for (const node of allNodes) {
|
|
55
|
+
for (const dep of node.getDependencies()) {
|
|
56
|
+
nodesInDependencies.add(dep);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const leafNodes = allNodes.filter(node => !nodesInDependencies.has(node));
|
|
61
|
+
|
|
62
|
+
if (leafNodes.length === 0) {
|
|
63
|
+
throw new Error("No leaf node found in DAG");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If multiple leaf nodes, take the last one in topological order
|
|
67
|
+
const leafNode = leafNodes[leafNodes.length - 1]!;
|
|
68
|
+
|
|
69
|
+
// Execute only the leaf node - it will get results from its dependencies
|
|
70
|
+
const result = leafNode.execute(context);
|
|
71
|
+
|
|
72
|
+
// If CTE is present, combine CTE SQL with main query
|
|
73
|
+
if (context.hasCTE && context.cteName && cteSql) {
|
|
74
|
+
// Combine CTE SQL with main query SQL
|
|
75
|
+
result.sql = cteSql + (result.sql ? '\n' + result.sql : '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a basic DAG for component-based queries
|
|
83
|
+
*/
|
|
84
|
+
public static buildBasicQuery(context: QueryContext): QueryDAG {
|
|
85
|
+
const dag = new QueryDAG();
|
|
86
|
+
|
|
87
|
+
// Count total filters across all components
|
|
88
|
+
let totalFilters = 0;
|
|
89
|
+
for (const filters of context.componentFilters.values()) {
|
|
90
|
+
totalFilters += filters.length;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If we have multiple component filters (>= 2), use CTE for optimization
|
|
94
|
+
const useCTE = totalFilters >= 2 && context.componentIds.size > 0;
|
|
95
|
+
|
|
96
|
+
if (useCTE) {
|
|
97
|
+
// Create CTE node as root
|
|
98
|
+
const cteNode = new CTENode();
|
|
99
|
+
dag.setRootNode(cteNode);
|
|
100
|
+
|
|
101
|
+
// If we have component requirements, add component inclusion node
|
|
102
|
+
if (context.componentIds.size > 0 || context.excludedComponentIds.size > 0) {
|
|
103
|
+
const componentNode = new ComponentInclusionNode();
|
|
104
|
+
componentNode.addDependency(cteNode);
|
|
105
|
+
dag.addNode(componentNode);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// Create source node
|
|
109
|
+
const sourceNode = new SourceNode();
|
|
110
|
+
dag.setRootNode(sourceNode);
|
|
111
|
+
|
|
112
|
+
// If we have component requirements, add component inclusion node
|
|
113
|
+
if (context.componentIds.size > 0 || context.excludedComponentIds.size > 0) {
|
|
114
|
+
const componentNode = new ComponentInclusionNode();
|
|
115
|
+
componentNode.addDependency(sourceNode);
|
|
116
|
+
dag.addNode(componentNode);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return dag;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { QueryContext } from "./QueryContext";
|
|
2
|
+
|
|
3
|
+
export interface QueryResult {
|
|
4
|
+
sql: string;
|
|
5
|
+
params: any[];
|
|
6
|
+
context: QueryContext;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export abstract class QueryNode {
|
|
10
|
+
protected dependencies: QueryNode[] = [];
|
|
11
|
+
protected dependents: QueryNode[] = [];
|
|
12
|
+
|
|
13
|
+
public addDependency(node: QueryNode): void {
|
|
14
|
+
if (!this.dependencies.includes(node)) {
|
|
15
|
+
this.dependencies.push(node);
|
|
16
|
+
node.addDependent(this);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public addDependent(node: QueryNode): void {
|
|
21
|
+
if (!this.dependents.includes(node)) {
|
|
22
|
+
this.dependents.push(node);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getDependencies(): QueryNode[] {
|
|
27
|
+
return [...this.dependencies];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public getDependents(): QueryNode[] {
|
|
31
|
+
return [...this.dependents];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public abstract execute(context: QueryContext): QueryResult;
|
|
35
|
+
|
|
36
|
+
public abstract getNodeType(): string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get all nodes in topological order (dependencies first)
|
|
40
|
+
*/
|
|
41
|
+
public getTopologicalOrder(visited: Set<QueryNode> = new Set(), result: QueryNode[] = []): QueryNode[] {
|
|
42
|
+
if (visited.has(this)) {
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
visited.add(this);
|
|
47
|
+
|
|
48
|
+
// Visit all dependencies first
|
|
49
|
+
for (const dep of this.dependencies) {
|
|
50
|
+
dep.getTopologicalOrder(visited, result);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Then add this node
|
|
54
|
+
if (!result.includes(this)) {
|
|
55
|
+
result.push(this);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Then visit all dependents (nodes that depend on this)
|
|
59
|
+
for (const dependent of this.dependents) {
|
|
60
|
+
dependent.getTopologicalOrder(visited, result);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { QueryNode } from "./QueryNode";
|
|
2
|
+
import type { QueryResult } from "./QueryNode";
|
|
3
|
+
import { QueryContext } from "./QueryContext";
|
|
4
|
+
|
|
5
|
+
export class SourceNode extends QueryNode {
|
|
6
|
+
public execute(context: QueryContext): QueryResult {
|
|
7
|
+
let sql = "SELECT id FROM entities WHERE deleted_at IS NULL";
|
|
8
|
+
|
|
9
|
+
if (context.withId) {
|
|
10
|
+
sql += ` AND id = $${context.addParam(context.withId)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Add entity exclusions if any
|
|
14
|
+
if (context.excludedEntityIds.size > 0) {
|
|
15
|
+
const excludedIds = Array.from(context.excludedEntityIds);
|
|
16
|
+
// Fix: Use the id directly instead of shift() which mutates the array
|
|
17
|
+
const placeholders = excludedIds.map((id) => `$${context.addParam(id)}`).join(', ');
|
|
18
|
+
sql += ` AND id NOT IN (${placeholders})`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Apply cursor-based pagination (more efficient than OFFSET)
|
|
22
|
+
if (context.cursorId !== null) {
|
|
23
|
+
const operator = context.cursorDirection === 'after' ? '>' : '<';
|
|
24
|
+
sql += ` AND id ${operator} $${context.addParam(context.cursorId)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Order by id - reverse for 'before' cursor direction
|
|
28
|
+
const orderDirection = context.cursorDirection === 'before' ? 'DESC' : 'ASC';
|
|
29
|
+
sql += ` ORDER BY id ${orderDirection}`;
|
|
30
|
+
|
|
31
|
+
// Only apply pagination if CTENode hasn't already applied it
|
|
32
|
+
// This prevents double parameter addition and incorrect SQL
|
|
33
|
+
if (!context.paginationAppliedInCTE) {
|
|
34
|
+
if (context.limit !== null) {
|
|
35
|
+
sql += ` LIMIT $${context.addParam(context.limit)}`;
|
|
36
|
+
}
|
|
37
|
+
// Only include OFFSET when not using cursor-based pagination
|
|
38
|
+
if (context.cursorId === null && (context.offsetValue > 0 || context.limit !== null)) {
|
|
39
|
+
sql += ` OFFSET $${context.addParam(context.offsetValue)}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
sql,
|
|
45
|
+
params: context.params,
|
|
46
|
+
context
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public getNodeType(): string {
|
|
51
|
+
return "SourceNode";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-Text Search Filter Builder
|
|
3
|
+
*
|
|
4
|
+
* Provides PostgreSQL full-text search capabilities as a custom filter builder.
|
|
5
|
+
* Demonstrates advanced filter builder patterns including validation and options.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FilterBuilder, FilterBuilderOptions } from "../FilterBuilder";
|
|
9
|
+
import type { QueryFilter } from "../QueryContext";
|
|
10
|
+
import type { QueryContext } from "../QueryContext";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Full-text search filter value interface
|
|
14
|
+
*/
|
|
15
|
+
export interface FullTextFilterValue {
|
|
16
|
+
/** The search query text */
|
|
17
|
+
query: string;
|
|
18
|
+
/** Optional language for text search (defaults to 'english') */
|
|
19
|
+
language?: string;
|
|
20
|
+
/** Optional search type: 'plain' (default), 'phrase', 'web', 'tsquery' */
|
|
21
|
+
type?: 'plain' | 'phrase' | 'web' | 'tsquery';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate full-text search filter values
|
|
26
|
+
*/
|
|
27
|
+
function validateFullTextFilter(filter: QueryFilter): boolean {
|
|
28
|
+
const value = filter.value as FullTextFilterValue;
|
|
29
|
+
|
|
30
|
+
if (!value || typeof value !== 'object') {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!value.query || typeof value.query !== 'string' || value.query.trim().length === 0) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (value.language && typeof value.language !== 'string') {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (value.type && !['plain', 'phrase', 'web', 'tsquery'].includes(value.type)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Full-text search filter builder using PostgreSQL's built-in text search
|
|
51
|
+
*
|
|
52
|
+
* Supports multiple search types:
|
|
53
|
+
* - plain: plainto_tsquery() - simple natural language search
|
|
54
|
+
* - phrase: phraseto_tsquery() - exact phrase matching
|
|
55
|
+
* - web: websearch_to_tsquery() - web-style search syntax
|
|
56
|
+
* - tsquery: raw tsquery syntax for advanced users
|
|
57
|
+
*
|
|
58
|
+
* @param filter - Filter containing FullTextFilterValue
|
|
59
|
+
* @param alias - Component table alias
|
|
60
|
+
* @param context - Query context for parameter management
|
|
61
|
+
* @returns SQL fragment for full-text search
|
|
62
|
+
*/
|
|
63
|
+
export const fullTextSearchBuilder: FilterBuilder = (
|
|
64
|
+
filter: QueryFilter,
|
|
65
|
+
alias: string,
|
|
66
|
+
context: QueryContext
|
|
67
|
+
): { sql: string; addedParams: number } => {
|
|
68
|
+
const value = filter.value as FullTextFilterValue;
|
|
69
|
+
const { query, language = 'english', type = 'plain' } = value;
|
|
70
|
+
|
|
71
|
+
// Build the text search vector from the specified field
|
|
72
|
+
const fieldPath = filter.field.includes('.')
|
|
73
|
+
? filter.field.split('.').map(p => `'${p}'`).join('->')
|
|
74
|
+
: `'${filter.field}'`;
|
|
75
|
+
|
|
76
|
+
const vectorSql = `to_tsvector('${language}', ${alias}.data->${fieldPath})`;
|
|
77
|
+
|
|
78
|
+
// Choose the appropriate query function based on type
|
|
79
|
+
let queryFunction: string;
|
|
80
|
+
switch (type) {
|
|
81
|
+
case 'phrase':
|
|
82
|
+
queryFunction = 'phraseto_tsquery';
|
|
83
|
+
break;
|
|
84
|
+
case 'web':
|
|
85
|
+
queryFunction = 'websearch_to_tsquery';
|
|
86
|
+
break;
|
|
87
|
+
case 'tsquery':
|
|
88
|
+
queryFunction = 'to_tsquery';
|
|
89
|
+
break;
|
|
90
|
+
case 'plain':
|
|
91
|
+
default:
|
|
92
|
+
queryFunction = 'plainto_tsquery';
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const querySql = `${queryFunction}('${language}', $${context.addParam(query)})`;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
sql: `${vectorSql} @@ ${querySql}`,
|
|
100
|
+
addedParams: 1
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Full-text search filter builder with ranking (returns relevance score)
|
|
106
|
+
*
|
|
107
|
+
* This builder includes ranking information that can be used for ordering results
|
|
108
|
+
* by relevance. Note: This requires modifying the SELECT clause to include the rank.
|
|
109
|
+
*
|
|
110
|
+
* @param filter - Filter containing FullTextFilterValue
|
|
111
|
+
* @param alias - Component table alias
|
|
112
|
+
* @param context - Query context for parameter management
|
|
113
|
+
* @returns SQL fragment with ranking
|
|
114
|
+
*/
|
|
115
|
+
export const fullTextSearchWithRankBuilder: FilterBuilder = (
|
|
116
|
+
filter: QueryFilter,
|
|
117
|
+
alias: string,
|
|
118
|
+
context: QueryContext
|
|
119
|
+
): { sql: string; addedParams: number } => {
|
|
120
|
+
const value = filter.value as FullTextFilterValue;
|
|
121
|
+
const { query, language = 'english', type = 'plain' } = value;
|
|
122
|
+
|
|
123
|
+
// Build the text search vector from the specified field
|
|
124
|
+
const fieldPath = filter.field.includes('.')
|
|
125
|
+
? filter.field.split('.').map(p => `'${p}'`).join('->')
|
|
126
|
+
: `'${filter.field}'`;
|
|
127
|
+
|
|
128
|
+
const vectorSql = `to_tsvector('${language}', ${alias}.data->${fieldPath})`;
|
|
129
|
+
|
|
130
|
+
// Choose the appropriate query function based on type
|
|
131
|
+
let queryFunction: string;
|
|
132
|
+
switch (type) {
|
|
133
|
+
case 'phrase':
|
|
134
|
+
queryFunction = 'phraseto_tsquery';
|
|
135
|
+
break;
|
|
136
|
+
case 'web':
|
|
137
|
+
queryFunction = 'websearch_to_tsquery';
|
|
138
|
+
break;
|
|
139
|
+
case 'tsquery':
|
|
140
|
+
queryFunction = 'to_tsquery';
|
|
141
|
+
break;
|
|
142
|
+
case 'plain':
|
|
143
|
+
default:
|
|
144
|
+
queryFunction = 'plainto_tsquery';
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const querySql = `${queryFunction}('${language}', $${context.addParam(query)})`;
|
|
149
|
+
|
|
150
|
+
// Include ranking in the condition (can be used for ordering)
|
|
151
|
+
const rankSql = `ts_rank(${vectorSql}, ${querySql})`;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
sql: `${vectorSql} @@ ${querySql} /* RANK: ${rankSql} */`,
|
|
155
|
+
addedParams: 1
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Full-text search filter builder options
|
|
161
|
+
*/
|
|
162
|
+
export const fullTextSearchOptions: FilterBuilderOptions = {
|
|
163
|
+
supportsLateral: true, // Full-text search works well with LATERAL joins
|
|
164
|
+
requiresIndex: true, // Benefits greatly from GIN indexes on tsvector columns
|
|
165
|
+
complexityScore: 3, // Moderate complexity due to text processing
|
|
166
|
+
validate: validateFullTextFilter
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Full-text search with ranking options
|
|
171
|
+
*/
|
|
172
|
+
export const fullTextSearchWithRankOptions: FilterBuilderOptions = {
|
|
173
|
+
supportsLateral: true,
|
|
174
|
+
requiresIndex: true,
|
|
175
|
+
complexityScore: 4, // Higher complexity due to ranking calculation
|
|
176
|
+
validate: validateFullTextFilter
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Helper function to create a full-text search filter with custom options
|
|
181
|
+
*
|
|
182
|
+
* @param language - Text search language (defaults to 'english')
|
|
183
|
+
* @param searchType - Search type ('plain', 'phrase', 'web', 'tsquery')
|
|
184
|
+
* @returns Configured filter builder and options
|
|
185
|
+
*/
|
|
186
|
+
export function createFullTextSearchBuilder(
|
|
187
|
+
language: string = 'english',
|
|
188
|
+
searchType: 'plain' | 'phrase' | 'web' | 'tsquery' = 'plain'
|
|
189
|
+
): { builder: FilterBuilder; options: FilterBuilderOptions } {
|
|
190
|
+
const builder: FilterBuilder = (filter: QueryFilter, alias: string, context: QueryContext) => {
|
|
191
|
+
const value = filter.value as FullTextFilterValue;
|
|
192
|
+
const query = value.query;
|
|
193
|
+
|
|
194
|
+
// Build the text search vector from the specified field
|
|
195
|
+
const fieldPath = filter.field.includes('.')
|
|
196
|
+
? filter.field.split('.').map(p => `'${p}'`).join('->')
|
|
197
|
+
: `'${filter.field}'`;
|
|
198
|
+
|
|
199
|
+
const vectorSql = `to_tsvector('${language}', ${alias}.data->${fieldPath})`;
|
|
200
|
+
|
|
201
|
+
// Choose the appropriate query function based on type
|
|
202
|
+
let queryFunction: string;
|
|
203
|
+
switch (searchType) {
|
|
204
|
+
case 'phrase':
|
|
205
|
+
queryFunction = 'phraseto_tsquery';
|
|
206
|
+
break;
|
|
207
|
+
case 'web':
|
|
208
|
+
queryFunction = 'websearch_to_tsquery';
|
|
209
|
+
break;
|
|
210
|
+
case 'tsquery':
|
|
211
|
+
queryFunction = 'to_tsquery';
|
|
212
|
+
break;
|
|
213
|
+
case 'plain':
|
|
214
|
+
default:
|
|
215
|
+
queryFunction = 'plainto_tsquery';
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const querySql = `${queryFunction}('${language}', $${context.addParam(query)})`;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
sql: `${vectorSql} @@ ${querySql}`,
|
|
223
|
+
addedParams: 1
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
builder,
|
|
229
|
+
options: {
|
|
230
|
+
supportsLateral: true,
|
|
231
|
+
requiresIndex: true,
|
|
232
|
+
complexityScore: 3,
|
|
233
|
+
validate: validateFullTextFilter
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
package/query/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { QueryContext } from "./QueryContext";
|
|
2
|
+
export { QueryNode, type QueryResult } from "./QueryNode";
|
|
3
|
+
export { SourceNode } from "./SourceNode";
|
|
4
|
+
export { ComponentInclusionNode } from "./ComponentInclusionNode";
|
|
5
|
+
export { QueryDAG } from "./QueryDAG";
|
|
6
|
+
export { OrQuery } from "./OrQuery";
|
|
7
|
+
export { OrNode } from "./OrNode";
|
|
8
|
+
export { Query, or } from "./Query";
|
|
9
|
+
|
|
10
|
+
export type FilterSchema<T = any> = {
|
|
11
|
+
[K in keyof T]?: {
|
|
12
|
+
field: string;
|
|
13
|
+
op: string;
|
|
14
|
+
value: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Custom Filter Builder exports
|
|
19
|
+
export type { FilterBuilder, FilterResult, FilterBuilderOptions } from "./FilterBuilder";
|
|
20
|
+
export { buildJSONPath } from "./FilterBuilder";
|
|
21
|
+
export { FilterBuilderRegistry } from "./FilterBuilderRegistry";
|