bunsane 0.3.2 → 0.5.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 (220) hide show
  1. package/CHANGELOG.md +471 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +93 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +8 -7
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +25 -10
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +383 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/getCacheManager.ts +10 -0
  30. package/core/entity/pendingOps.ts +72 -0
  31. package/core/entity/saveEntity.ts +375 -0
  32. package/core/health.ts +93 -4
  33. package/core/hooks/dispatcher.ts +439 -0
  34. package/core/hooks/guards.ts +155 -0
  35. package/core/hooks/registry.ts +247 -0
  36. package/core/metadata/definitions/Component.ts +1 -1
  37. package/core/metadata/index.ts +15 -4
  38. package/core/middleware/RateLimit.ts +102 -105
  39. package/core/middleware/RequestId.ts +2 -9
  40. package/core/middleware/SecurityHeaders.ts +2 -11
  41. package/core/middleware/headers.ts +28 -0
  42. package/core/remote/OutboxWorker.ts +213 -183
  43. package/core/remote/RemoteManager.ts +401 -400
  44. package/core/remote/StreamConsumer.ts +535 -535
  45. package/core/remote/types.ts +153 -151
  46. package/core/requestScope.ts +34 -0
  47. package/core/scheduler/cronEvaluator.ts +174 -0
  48. package/core/scheduler/lifecycleHooks.ts +21 -0
  49. package/core/scheduler/lockCoordinator.ts +27 -0
  50. package/core/scheduler/metrics.ts +14 -0
  51. package/core/scheduler/taskRunner.ts +420 -0
  52. package/core/validateEnv.ts +10 -0
  53. package/database/DatabaseHelper.ts +128 -101
  54. package/database/IndexingStrategy.ts +72 -2
  55. package/database/PreparedStatementCache.ts +8 -2
  56. package/database/cancellable.ts +35 -22
  57. package/database/index.ts +29 -3
  58. package/database/instrumentedDb.ts +141 -141
  59. package/database/sqlHelpers.ts +3 -1
  60. package/endpoints/archetypes.ts +2 -8
  61. package/endpoints/tables.ts +6 -1
  62. package/gql/index.ts +1 -1
  63. package/gql/schema/index.ts +15 -4
  64. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  65. package/package.json +22 -1
  66. package/query/CTENode.ts +5 -3
  67. package/query/ComponentInclusionNode.ts +245 -14
  68. package/query/OrNode.ts +8 -19
  69. package/query/Query.ts +208 -79
  70. package/query/QueryContext.ts +6 -0
  71. package/query/QueryDAG.ts +7 -2
  72. package/query/membershipSource.ts +66 -0
  73. package/storage/LocalStorageProvider.ts +8 -3
  74. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  75. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  76. package/studio/{index.html → dist/index.html} +3 -2
  77. package/swagger/generator.ts +11 -1
  78. package/upload/UploadManager.ts +8 -6
  79. package/utils/uuid.ts +40 -10
  80. package/.claude/scheduled_tasks.lock +0 -1
  81. package/.claude/settings.local.json +0 -47
  82. package/.prettierrc +0 -4
  83. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  84. package/.serena/memories/architecture.md +0 -154
  85. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  86. package/.serena/memories/code_style_and_conventions.md +0 -76
  87. package/.serena/memories/project_overview.md +0 -43
  88. package/.serena/memories/schema-dsl-plan.md +0 -107
  89. package/.serena/memories/suggested_commands.md +0 -80
  90. package/.serena/memories/typescript-compilation-status.md +0 -54
  91. package/.serena/project.yml +0 -114
  92. package/BunSane.jpg +0 -0
  93. package/CLAUDE.md +0 -198
  94. package/TODO.md +0 -2
  95. package/bun.lock +0 -302
  96. package/bunfig.toml +0 -10
  97. package/docs/RFC_APP_REFACTOR.md +0 -248
  98. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  99. package/docs/SCALABILITY_PLAN.md +0 -175
  100. package/studio/bun.lock +0 -482
  101. package/studio/package.json +0 -39
  102. package/studio/postcss.config.js +0 -6
  103. package/studio/src/components/DataTable.tsx +0 -211
  104. package/studio/src/components/Layout.tsx +0 -13
  105. package/studio/src/components/PageContainer.tsx +0 -9
  106. package/studio/src/components/PageHeader.tsx +0 -13
  107. package/studio/src/components/SearchBar.tsx +0 -57
  108. package/studio/src/components/Sidebar.tsx +0 -294
  109. package/studio/src/components/ui/button.tsx +0 -56
  110. package/studio/src/components/ui/checkbox.tsx +0 -26
  111. package/studio/src/components/ui/input.tsx +0 -25
  112. package/studio/src/hooks/useDataTable.ts +0 -131
  113. package/studio/src/index.css +0 -36
  114. package/studio/src/lib/api.ts +0 -186
  115. package/studio/src/lib/utils.ts +0 -13
  116. package/studio/src/main.tsx +0 -17
  117. package/studio/src/pages/ArcheType.tsx +0 -239
  118. package/studio/src/pages/Components.tsx +0 -124
  119. package/studio/src/pages/EntityInspector.tsx +0 -302
  120. package/studio/src/pages/QueryRunner.tsx +0 -246
  121. package/studio/src/pages/Table.tsx +0 -94
  122. package/studio/src/pages/Welcome.tsx +0 -241
  123. package/studio/src/routes.tsx +0 -45
  124. package/studio/src/store/archeTypeSettings.ts +0 -30
  125. package/studio/src/store/studio.ts +0 -65
  126. package/studio/src/utils/columnHelpers.tsx +0 -114
  127. package/studio/studio-instructions.md +0 -81
  128. package/studio/tailwind.config.js +0 -77
  129. package/studio/utils.ts +0 -54
  130. package/studio/vite.config.js +0 -19
  131. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  132. package/tests/benchmark/bunfig.toml +0 -9
  133. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  134. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  135. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  136. package/tests/benchmark/fixtures/index.ts +0 -6
  137. package/tests/benchmark/index.ts +0 -22
  138. package/tests/benchmark/noop-preload.ts +0 -3
  139. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  140. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  141. package/tests/benchmark/runners/index.ts +0 -4
  142. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  143. package/tests/benchmark/scripts/generate-db.ts +0 -344
  144. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  145. package/tests/e2e/http.test.ts +0 -130
  146. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  147. package/tests/fixtures/components/TestOrder.ts +0 -23
  148. package/tests/fixtures/components/TestProduct.ts +0 -23
  149. package/tests/fixtures/components/TestUser.ts +0 -20
  150. package/tests/fixtures/components/index.ts +0 -6
  151. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  152. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  153. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  154. package/tests/helpers/MockRedisClient.ts +0 -113
  155. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  156. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  157. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  158. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  159. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  160. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  161. package/tests/integration/query/Query.abort.test.ts +0 -66
  162. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  163. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  164. package/tests/integration/query/Query.exec.test.ts +0 -576
  165. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  166. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  167. package/tests/integration/remote/dlq.test.ts +0 -175
  168. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  169. package/tests/integration/remote/outbox.test.ts +0 -130
  170. package/tests/integration/remote/rpc.test.ts +0 -177
  171. package/tests/pglite-setup.ts +0 -62
  172. package/tests/setup.ts +0 -164
  173. package/tests/stress/BenchmarkRunner.ts +0 -203
  174. package/tests/stress/DataSeeder.ts +0 -190
  175. package/tests/stress/StressTestReporter.ts +0 -229
  176. package/tests/stress/cursor-perf-test.ts +0 -171
  177. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  178. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  179. package/tests/stress/index.ts +0 -7
  180. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  181. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  182. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  183. package/tests/unit/BatchLoader.test.ts +0 -196
  184. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  185. package/tests/unit/cache/CacheManager.test.ts +0 -498
  186. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  187. package/tests/unit/cache/RedisCache.test.ts +0 -411
  188. package/tests/unit/database/cancellable.test.ts +0 -81
  189. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  190. package/tests/unit/entity/Entity.components.test.ts +0 -317
  191. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  192. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  193. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  194. package/tests/unit/entity/Entity.test.ts +0 -345
  195. package/tests/unit/gql/depthLimit.test.ts +0 -203
  196. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  197. package/tests/unit/health/Health.test.ts +0 -129
  198. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  199. package/tests/unit/middleware/Middleware.test.ts +0 -98
  200. package/tests/unit/middleware/RequestId.test.ts +0 -54
  201. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  202. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  203. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  204. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  205. package/tests/unit/query/Query.test.ts +0 -310
  206. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  207. package/tests/unit/remote/RemoteError.test.ts +0 -55
  208. package/tests/unit/remote/decorators.test.ts +0 -195
  209. package/tests/unit/remote/metrics.test.ts +0 -115
  210. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  211. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  212. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  213. package/tests/unit/schema/schema-integration.test.ts +0 -426
  214. package/tests/unit/schema/schema.test.ts +0 -580
  215. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  216. package/tests/unit/upload/RestUpload.test.ts +0 -267
  217. package/tests/unit/validateEnv.test.ts +0 -82
  218. package/tests/utils/entity-tracker.ts +0 -57
  219. package/tests/utils/index.ts +0 -13
  220. package/tests/utils/test-context.ts +0 -149
@@ -1,111 +0,0 @@
1
- /**
2
- * Unit tests for FilterBuilder utility functions
3
- */
4
- import { describe, test, expect } from 'bun:test';
5
- import { buildJSONPath, composeFilters, type FilterBuilder, type FilterResult } from '../../../query/FilterBuilder';
6
-
7
- describe('FilterBuilder', () => {
8
- describe('buildJSONPath()', () => {
9
- test('builds simple field path', () => {
10
- const result = buildJSONPath('name', 'c');
11
- expect(result).toBe("c.data->>'name'");
12
- });
13
-
14
- test('builds nested field path', () => {
15
- const result = buildJSONPath('location.latitude', 'c');
16
- expect(result).toBe("c.data->'location'->>'latitude'");
17
- });
18
-
19
- test('builds deeply nested path', () => {
20
- const result = buildJSONPath('device.location.coordinates.latitude', 'c');
21
- expect(result).toBe("c.data->'device'->'location'->'coordinates'->>'latitude'");
22
- });
23
-
24
- test('uses provided alias', () => {
25
- const result = buildJSONPath('field', 'comp');
26
- expect(result).toBe("comp.data->>'field'");
27
- });
28
- });
29
-
30
- describe('composeFilters()', () => {
31
- test('throws for empty array', () => {
32
- expect(() => composeFilters([])).toThrow('Cannot compose empty array of filter builders');
33
- });
34
-
35
- test('composes single builder', () => {
36
- const mockBuilder: FilterBuilder = () => ({
37
- sql: 'field = $1',
38
- addedParams: 1
39
- });
40
-
41
- const composed = composeFilters([mockBuilder]);
42
- const result = composed(
43
- { field: 'test', operator: 'EQ', value: 'value' },
44
- 'c',
45
- {} as any
46
- );
47
-
48
- expect(result.sql).toBe('(field = $1)');
49
- expect(result.addedParams).toBe(1);
50
- });
51
-
52
- test('composes multiple builders with AND', () => {
53
- const builder1: FilterBuilder = () => ({
54
- sql: 'field1 = $1',
55
- addedParams: 1
56
- });
57
- const builder2: FilterBuilder = () => ({
58
- sql: 'field2 > $2',
59
- addedParams: 1
60
- });
61
-
62
- const composed = composeFilters([builder1, builder2]);
63
- const result = composed(
64
- { field: 'test', operator: 'EQ', value: 'value' },
65
- 'c',
66
- {} as any
67
- );
68
-
69
- expect(result.sql).toBe('(field1 = $1) AND (field2 > $2)');
70
- expect(result.addedParams).toBe(2);
71
- });
72
-
73
- test('handles empty SQL from builder', () => {
74
- const builder1: FilterBuilder = () => ({
75
- sql: 'field1 = $1',
76
- addedParams: 1
77
- });
78
- const builder2: FilterBuilder = () => ({
79
- sql: '',
80
- addedParams: 0
81
- });
82
-
83
- const composed = composeFilters([builder1, builder2]);
84
- const result = composed(
85
- { field: 'test', operator: 'EQ', value: 'value' },
86
- 'c',
87
- {} as any
88
- );
89
-
90
- expect(result.sql).toBe('(field1 = $1)');
91
- expect(result.addedParams).toBe(1);
92
- });
93
-
94
- test('handles whitespace-only SQL', () => {
95
- const builder: FilterBuilder = () => ({
96
- sql: ' ',
97
- addedParams: 0
98
- });
99
-
100
- const composed = composeFilters([builder]);
101
- const result = composed(
102
- { field: 'test', operator: 'EQ', value: 'value' },
103
- 'c',
104
- {} as any
105
- );
106
-
107
- expect(result.sql).toBe('');
108
- expect(result.addedParams).toBe(0);
109
- });
110
- });
111
- });
@@ -1,178 +0,0 @@
1
- /**
2
- * Unit tests for JSONB Array Filter Builders
3
- */
4
- import { describe, test, expect } from 'bun:test';
5
- import { buildJSONBPath } from '../../../query/FilterBuilder';
6
- import { QueryContext } from '../../../query/QueryContext';
7
- import {
8
- jsonbContainsBuilder,
9
- jsonbContainedByBuilder,
10
- jsonbHasAnyBuilder,
11
- jsonbHasAllBuilder,
12
- jsonbArrayOptions,
13
- } from '../../../query/builders/JsonbArrayBuilder';
14
-
15
- describe('buildJSONBPath', () => {
16
- test('simple field returns JSONB node path', () => {
17
- expect(buildJSONBPath('tags', 'c')).toBe("c.data->'tags'");
18
- });
19
-
20
- test('nested field returns JSONB node path', () => {
21
- expect(buildJSONBPath('metadata.tags', 'c')).toBe("c.data->'metadata'->'tags'");
22
- });
23
-
24
- test('deeply nested field', () => {
25
- expect(buildJSONBPath('a.b.c', 'c')).toBe("c.data->'a'->'b'->'c'");
26
- });
27
-
28
- test('uses provided alias', () => {
29
- expect(buildJSONBPath('tags', 'comp')).toBe("comp.data->'tags'");
30
- });
31
- });
32
-
33
- describe('jsonbContainsBuilder (@>)', () => {
34
- test('single string value is auto-wrapped in array', () => {
35
- const ctx = new QueryContext();
36
- const result = jsonbContainsBuilder(
37
- { field: 'tags', operator: 'CONTAINS', value: 'urgent' },
38
- 'c', ctx
39
- );
40
- expect(result.sql).toBe("c.data->'tags' @> $1::jsonb");
41
- expect(ctx.params[0]).toEqual(['urgent']);
42
- expect(result.addedParams).toBe(1);
43
- });
44
-
45
- test('array value is passed as raw array', () => {
46
- const ctx = new QueryContext();
47
- const result = jsonbContainsBuilder(
48
- { field: 'tags', operator: 'CONTAINS', value: ['a', 'b'] },
49
- 'c', ctx
50
- );
51
- expect(result.sql).toBe("c.data->'tags' @> $1::jsonb");
52
- expect(ctx.params[0]).toEqual(['a', 'b']);
53
- });
54
-
55
- test('nested field path', () => {
56
- const ctx = new QueryContext();
57
- const result = jsonbContainsBuilder(
58
- { field: 'meta.tags', operator: 'CONTAINS', value: 'x' },
59
- 'c', ctx
60
- );
61
- expect(result.sql).toBe("c.data->'meta'->'tags' @> $1::jsonb");
62
- });
63
-
64
- test('numeric value', () => {
65
- const ctx = new QueryContext();
66
- jsonbContainsBuilder(
67
- { field: 'scores', operator: 'CONTAINS', value: 42 },
68
- 'c', ctx
69
- );
70
- expect(ctx.params[0]).toEqual([42]);
71
- });
72
- });
73
-
74
- describe('jsonbContainedByBuilder (<@)', () => {
75
- test('generates correct SQL', () => {
76
- const ctx = new QueryContext();
77
- const result = jsonbContainedByBuilder(
78
- { field: 'tags', operator: 'CONTAINED_BY', value: ['a', 'b', 'c'] },
79
- 'c', ctx
80
- );
81
- expect(result.sql).toBe("c.data->'tags' <@ $1::jsonb");
82
- expect(ctx.params[0]).toEqual(['a', 'b', 'c']);
83
- expect(result.addedParams).toBe(1);
84
- });
85
-
86
- test('single value is auto-wrapped', () => {
87
- const ctx = new QueryContext();
88
- jsonbContainedByBuilder(
89
- { field: 'tags', operator: 'CONTAINED_BY', value: 'only' },
90
- 'c', ctx
91
- );
92
- expect(ctx.params[0]).toEqual(['only']);
93
- });
94
- });
95
-
96
- describe('jsonbHasAnyBuilder (?|)', () => {
97
- test('generates correct SQL with text[] cast', () => {
98
- const ctx = new QueryContext();
99
- const result = jsonbHasAnyBuilder(
100
- { field: 'tags', operator: 'HAS_ANY', value: ['a', 'b'] },
101
- 'c', ctx
102
- );
103
- expect(result.sql).toBe("c.data->'tags' ?| $1::text[]");
104
- expect(ctx.params[0]).toEqual(['a', 'b']);
105
- expect(result.addedParams).toBe(1);
106
- });
107
-
108
- test('single value is auto-wrapped and stringified', () => {
109
- const ctx = new QueryContext();
110
- jsonbHasAnyBuilder(
111
- { field: 'tags', operator: 'HAS_ANY', value: 'solo' },
112
- 'c', ctx
113
- );
114
- expect(ctx.params[0]).toEqual(['solo']);
115
- });
116
-
117
- test('numeric values are cast to strings', () => {
118
- const ctx = new QueryContext();
119
- jsonbHasAnyBuilder(
120
- { field: 'ids', operator: 'HAS_ANY', value: [1, 2, 3] },
121
- 'c', ctx
122
- );
123
- expect(ctx.params[0]).toEqual(['1', '2', '3']);
124
- });
125
- });
126
-
127
- describe('jsonbHasAllBuilder (?&)', () => {
128
- test('generates correct SQL with text[] cast', () => {
129
- const ctx = new QueryContext();
130
- const result = jsonbHasAllBuilder(
131
- { field: 'tags', operator: 'HAS_ALL', value: ['x', 'y'] },
132
- 'c', ctx
133
- );
134
- expect(result.sql).toBe("c.data->'tags' ?& $1::text[]");
135
- expect(ctx.params[0]).toEqual(['x', 'y']);
136
- expect(result.addedParams).toBe(1);
137
- });
138
- });
139
-
140
- describe('validation', () => {
141
- const validate = jsonbArrayOptions.validate!;
142
-
143
- test('rejects null value', () => {
144
- expect(validate({ field: 'f', operator: 'CONTAINS', value: null })).toBe(false);
145
- });
146
-
147
- test('rejects undefined value', () => {
148
- expect(validate({ field: 'f', operator: 'CONTAINS', value: undefined })).toBe(false);
149
- });
150
-
151
- test('rejects empty array', () => {
152
- expect(validate({ field: 'f', operator: 'CONTAINS', value: [] })).toBe(false);
153
- });
154
-
155
- test('accepts string value', () => {
156
- expect(validate({ field: 'f', operator: 'CONTAINS', value: 'tag' })).toBe(true);
157
- });
158
-
159
- test('accepts number value', () => {
160
- expect(validate({ field: 'f', operator: 'CONTAINS', value: 42 })).toBe(true);
161
- });
162
-
163
- test('accepts boolean value', () => {
164
- expect(validate({ field: 'f', operator: 'CONTAINS', value: true })).toBe(true);
165
- });
166
-
167
- test('accepts array of strings', () => {
168
- expect(validate({ field: 'f', operator: 'CONTAINS', value: ['a', 'b'] })).toBe(true);
169
- });
170
-
171
- test('rejects object value', () => {
172
- expect(validate({ field: 'f', operator: 'CONTAINS', value: { key: 'val' } })).toBe(false);
173
- });
174
-
175
- test('rejects array with non-primitive elements', () => {
176
- expect(validate({ field: 'f', operator: 'CONTAINS', value: [{ a: 1 }] })).toBe(false);
177
- });
178
- });
@@ -1,69 +0,0 @@
1
- /**
2
- * Empty-string filter support. JSONB text extraction (c.data->>'field')
3
- * returns text, so `= ''` / `!= ''` / LIKE against empty string are
4
- * legitimate. UUID-cast path is gated on a regex that empty cannot match.
5
- */
6
- import { describe, test, expect, beforeAll } from 'bun:test';
7
- import { Entity } from '../../../core/Entity';
8
- import { BaseComponent } from '../../../core/components/BaseComponent';
9
- import { Component, CompData } from '../../../core/components/Decorators';
10
- import { Query, FilterOp } from '../../../query/Query';
11
- import { ensureComponentsRegistered } from '../../utils';
12
-
13
- @Component
14
- class EmptyableNote extends BaseComponent {
15
- @CompData()
16
- value: string = '';
17
- }
18
-
19
- describe('Query empty-string filter', () => {
20
- beforeAll(async () => {
21
- await ensureComponentsRegistered(EmptyableNote);
22
- });
23
-
24
- test('Query.filter accepts empty-string value without throwing', () => {
25
- expect(() => Query.filter('value', FilterOp.EQ, '')).not.toThrow();
26
- const f = Query.filter('value', FilterOp.EQ, '');
27
- expect(f.value).toBe('');
28
- });
29
-
30
- test('Query.filter accepts whitespace-only value without throwing', () => {
31
- expect(() => Query.filter('value', FilterOp.EQ, ' ')).not.toThrow();
32
- });
33
-
34
- test('.with(C, filter EQ "") executes and returns matching rows', async () => {
35
- const withEmpty = Entity.Create();
36
- withEmpty.add(EmptyableNote, { value: '' });
37
- await withEmpty.save();
38
-
39
- const withData = Entity.Create();
40
- withData.add(EmptyableNote, { value: 'not empty' });
41
- await withData.save();
42
-
43
- const rows = await new Query()
44
- .with(EmptyableNote, Query.filters(Query.filter('value', FilterOp.EQ, '')))
45
- .exec();
46
-
47
- const ids = rows.map(e => e.id);
48
- expect(ids).toContain(withEmpty.id);
49
- expect(ids).not.toContain(withData.id);
50
- });
51
-
52
- test('.with(C, filter != "") excludes rows with empty value', async () => {
53
- const withEmpty = Entity.Create();
54
- withEmpty.add(EmptyableNote, { value: '' });
55
- await withEmpty.save();
56
-
57
- const withData = Entity.Create();
58
- withData.add(EmptyableNote, { value: 'populated' });
59
- await withData.save();
60
-
61
- const rows = await new Query()
62
- .with(EmptyableNote, Query.filters(Query.filter('value', FilterOp.NEQ, '')))
63
- .exec();
64
-
65
- const ids = rows.map(e => e.id);
66
- expect(ids).toContain(withData.id);
67
- expect(ids).not.toContain(withEmpty.id);
68
- });
69
- });
@@ -1,310 +0,0 @@
1
- /**
2
- * Unit tests for Query class
3
- * Tests query builder methods and structure
4
- */
5
- import { describe, test, expect, beforeAll } from 'bun:test';
6
- import { Query, FilterOp } from '../../../query/Query';
7
- import { TestUser, TestProduct } from '../../fixtures/components';
8
- import { ensureComponentsRegistered } from '../../utils';
9
-
10
- describe('Query', () => {
11
- beforeAll(async () => {
12
- await ensureComponentsRegistered(TestUser, TestProduct);
13
- });
14
-
15
- describe('constructor', () => {
16
- test('creates a new query instance', () => {
17
- const query = new Query();
18
- expect(query).toBeDefined();
19
- });
20
-
21
- test('creates query with transaction', () => {
22
- // Query accepts optional transaction parameter
23
- const query = new Query();
24
- expect(query).toBeDefined();
25
- });
26
- });
27
-
28
- describe('findById()', () => {
29
- test('sets up query to find by ID', () => {
30
- const query = new Query();
31
- const result = query.findById('test-id-123');
32
- expect(result).toBe(query); // Returns this for chaining
33
- });
34
-
35
- test('throws for empty string id', () => {
36
- const query = new Query();
37
- expect(() => query.findById('')).toThrow();
38
- });
39
-
40
- test('throws for whitespace id', () => {
41
- const query = new Query();
42
- expect(() => query.findById(' ')).toThrow();
43
- });
44
- });
45
-
46
- describe('with()', () => {
47
- test('adds component requirement to query', () => {
48
- const query = new Query();
49
- const result = query.with(TestUser);
50
- // with() returns same instance for chaining (type changes for type accumulation)
51
- expect(result as any).toBe(query as any);
52
- });
53
-
54
- test('allows chaining multiple with() calls', () => {
55
- const query = new Query();
56
- const result = query.with(TestUser).with(TestProduct);
57
- expect(result as any).toBe(query as any);
58
- });
59
-
60
- test('accepts component with filters', () => {
61
- const query = new Query();
62
- const result = query.with(TestUser, {
63
- filters: [{ field: 'name', operator: FilterOp.EQ, value: 'John' }]
64
- });
65
- expect(result as any).toBe(query as any);
66
- });
67
-
68
- test('accepts array of components with filters', () => {
69
- const query = new Query();
70
- const result = query.with([
71
- { component: TestUser, filters: [{ field: 'age', operator: FilterOp.GT, value: 18 }] },
72
- { component: TestProduct, filters: [{ field: 'price', operator: FilterOp.LT, value: 100 }] }
73
- ]);
74
- expect(result).toBe(query);
75
- });
76
- });
77
-
78
- describe('without()', () => {
79
- test('excludes entities with specified component', () => {
80
- const query = new Query();
81
- const result = query.with(TestUser).without(TestProduct);
82
- expect(result as any).toBe(query as any);
83
- });
84
- });
85
-
86
- describe('excludeEntityId()', () => {
87
- test('excludes specific entity from results', () => {
88
- const query = new Query();
89
- const result = query.excludeEntityId('entity-to-exclude');
90
- expect(result).toBe(query);
91
- });
92
- });
93
-
94
- describe('populate()', () => {
95
- test('enables component population', () => {
96
- const query = new Query();
97
- const result = query.with(TestUser).populate();
98
- expect(result as any).toBe(query as any);
99
- });
100
- });
101
-
102
- describe('eagerLoadComponents()', () => {
103
- test('sets up eager loading for components', () => {
104
- const query = new Query();
105
- const result = query.with(TestUser).eagerLoadComponents([TestUser, TestProduct]);
106
- expect(result as any).toBe(query as any);
107
- });
108
- });
109
-
110
- describe('eagerLoad()', () => {
111
- test('is an alias for eagerLoadComponents', () => {
112
- const query = new Query();
113
- const result = query.with(TestUser).eagerLoad([TestUser]);
114
- expect(result as any).toBe(query as any);
115
- });
116
- });
117
-
118
- describe('take()', () => {
119
- test('sets limit on query results', () => {
120
- const query = new Query();
121
- const result = query.with(TestUser).take(10);
122
- expect(result as any).toBe(query as any);
123
- });
124
- });
125
-
126
- describe('offset()', () => {
127
- test('sets offset for pagination', () => {
128
- const query = new Query();
129
- const result = query.with(TestUser).offset(20);
130
- expect(result as any).toBe(query as any);
131
- });
132
- });
133
-
134
- describe('sortBy()', () => {
135
- test('sets sort order', () => {
136
- const query = new Query();
137
- const result = query.with(TestUser).sortBy(TestUser, 'name', 'ASC');
138
- expect(result as any).toBe(query as any);
139
- });
140
-
141
- test('accepts DESC direction', () => {
142
- const query = new Query();
143
- const result = query.with(TestUser).sortBy(TestUser, 'age', 'DESC');
144
- expect(result as any).toBe(query as any);
145
- });
146
-
147
- test('accepts nullsFirst option', () => {
148
- const query = new Query();
149
- const result = query.with(TestUser).sortBy(TestUser, 'bio', 'ASC', true);
150
- expect(result as any).toBe(query as any);
151
- });
152
-
153
- test('throws if component not in query', () => {
154
- const query = new Query();
155
- expect(() => query.sortBy(TestUser, 'name')).toThrow();
156
- });
157
- });
158
-
159
- describe('debugMode()', () => {
160
- test('enables debug mode', () => {
161
- const query = new Query();
162
- const result = query.debugMode(true);
163
- expect(result).toBe(query);
164
- });
165
-
166
- test('can disable debug mode', () => {
167
- const query = new Query();
168
- const result = query.debugMode(false);
169
- expect(result).toBe(query);
170
- });
171
- });
172
-
173
- describe('noCache()', () => {
174
- test('bypasses prepared statement cache by default', () => {
175
- const query = new Query();
176
- const result = query.noCache();
177
- expect(result).toBe(query);
178
- });
179
-
180
- test('accepts cache options', () => {
181
- const query = new Query();
182
- const result = query.noCache({ preparedStatement: true, component: true });
183
- expect(result).toBe(query);
184
- });
185
- });
186
-
187
- describe('filter()', () => {
188
- test('creates filter object', () => {
189
- const filter = Query.filter('name', FilterOp.EQ, 'John');
190
- expect(filter.field).toBe('name');
191
- expect(filter.operator).toBe(FilterOp.EQ);
192
- expect(filter.value).toBe('John');
193
- });
194
-
195
- test('accepts empty string value', () => {
196
- const filter = Query.filter('name', FilterOp.EQ, '');
197
- expect(filter.value).toBe('');
198
- });
199
-
200
- test('accepts whitespace value', () => {
201
- const filter = Query.filter('name', FilterOp.EQ, ' ');
202
- expect(filter.value).toBe(' ');
203
- });
204
- });
205
-
206
- describe('typedFilter()', () => {
207
- test('creates typed filter object', () => {
208
- const filter = Query.typedFilter(TestUser, 'name', FilterOp.EQ, 'John');
209
- expect(filter.field).toBe('name');
210
- expect(filter.operator).toBe(FilterOp.EQ);
211
- expect(filter.value).toBe('John');
212
- });
213
- });
214
-
215
- describe('filters()', () => {
216
- test('creates filter options from multiple filters', () => {
217
- const filter1 = Query.filter('name', FilterOp.EQ, 'John');
218
- const filter2 = Query.filter('age', FilterOp.GT, 18);
219
- const options = Query.filters(filter1, filter2);
220
-
221
- expect(options.filters).toBeDefined();
222
- expect(options.filters?.length).toBe(2);
223
- });
224
- });
225
-
226
- describe('FilterOp', () => {
227
- test('has all expected operators', () => {
228
- expect(FilterOp.EQ).toBeDefined();
229
- expect(FilterOp.NEQ).toBeDefined();
230
- expect(FilterOp.GT).toBeDefined();
231
- expect(FilterOp.GTE).toBeDefined();
232
- expect(FilterOp.LT).toBeDefined();
233
- expect(FilterOp.LTE).toBeDefined();
234
- expect(FilterOp.LIKE).toBeDefined();
235
- expect(FilterOp.IN).toBeDefined();
236
- expect(FilterOp.NOT_IN).toBeDefined();
237
- });
238
- });
239
-
240
- describe('chaining', () => {
241
- test('supports full query chain', () => {
242
- const query = new Query()
243
- .with(TestUser, { filters: [Query.filter('age', FilterOp.GTE, 18)] })
244
- .with(TestProduct)
245
- .without(TestProduct)
246
- .take(10)
247
- .offset(0)
248
- .sortBy(TestUser, 'name')
249
- .populate()
250
- .debugMode(false)
251
- .noCache();
252
-
253
- expect(query).toBeDefined();
254
- });
255
- });
256
-
257
- describe('sum()', () => {
258
- test('throws if component not in query', async () => {
259
- const query = new Query();
260
- // TestProduct not added via .with()
261
- await expect(query.sum(TestProduct, 'price')).rejects.toThrow(
262
- /not included in the query/
263
- );
264
- });
265
-
266
- test('throws if component is not registered', async () => {
267
- class UnregisteredComponent {
268
- value!: number;
269
- }
270
- const query = new Query();
271
- // @ts-expect-error - Testing with unregistered component
272
- await expect(query.sum(UnregisteredComponent, 'value')).rejects.toThrow(
273
- /not registered/
274
- );
275
- });
276
-
277
- test('returns a Promise', () => {
278
- const query = new Query().with(TestProduct);
279
- const result = query.sum(TestProduct, 'price');
280
- expect(result).toBeInstanceOf(Promise);
281
- });
282
- });
283
-
284
- describe('average()', () => {
285
- test('throws if component not in query', async () => {
286
- const query = new Query();
287
- // TestProduct not added via .with()
288
- await expect(query.average(TestProduct, 'price')).rejects.toThrow(
289
- /not included in the query/
290
- );
291
- });
292
-
293
- test('throws if component is not registered', async () => {
294
- class UnregisteredComponent {
295
- value!: number;
296
- }
297
- const query = new Query();
298
- // @ts-expect-error - Testing with unregistered component
299
- await expect(query.average(UnregisteredComponent, 'value')).rejects.toThrow(
300
- /not registered/
301
- );
302
- });
303
-
304
- test('returns a Promise', () => {
305
- const query = new Query().with(TestProduct);
306
- const result = query.average(TestProduct, 'price');
307
- expect(result).toBeInstanceOf(Promise);
308
- });
309
- });
310
- });