bunsane 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. package/tests/scheduler.test.ts +0 -724
@@ -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";